@simplysm/sd-cli 12.16.11 → 12.16.13

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.
@@ -4,7 +4,7 @@ export declare class SdCliCapacitor {
4
4
  private readonly _CAPACITOR_DIR_NAME;
5
5
  private readonly _CONFIG_FILE_NAME;
6
6
  private readonly _KEYSTORE_FILE_NAME;
7
- private readonly _ICON_DIR_PATH;
7
+ private readonly _ICON_DIR_NAME;
8
8
  private readonly _platforms;
9
9
  private readonly _npmConfig;
10
10
  constructor(_opt: {
@@ -15,7 +15,6 @@ export declare class SdCliCapacitor {
15
15
  private static _execAsync;
16
16
  initializeAsync(): Promise<void>;
17
17
  private _initializeCapacitorProjectAsync;
18
- private _syncVersionAsync;
19
18
  private _createCapacitorConfigAsync;
20
19
  private _managePlatformsAsync;
21
20
  private _managePluginsAsync;
@@ -33,7 +32,6 @@ export declare class SdCliCapacitor {
33
32
  private _configureAndroidBuildGradleAsync;
34
33
  private _configureAndroidStringsAsync;
35
34
  buildAsync(outPath: string): Promise<void>;
36
- private _buildPlatformAsync;
37
35
  private _buildAndroidAsync;
38
36
  private _copyAndroidBuildOutputAsync;
39
37
  private _findAndroidSdk;
@@ -9,7 +9,7 @@ export class SdCliCapacitor {
9
9
  this._CAPACITOR_DIR_NAME = ".capacitor";
10
10
  this._CONFIG_FILE_NAME = "capacitor.config.ts";
11
11
  this._KEYSTORE_FILE_NAME = "android.keystore";
12
- this._ICON_DIR_PATH = "resources";
12
+ this._ICON_DIR_NAME = "resources";
13
13
  this._platforms = Object.keys(this._opt.config.platform ?? {});
14
14
  this._npmConfig = FsUtils.readJson(path.resolve(this._opt.pkgPath, "package.json"));
15
15
  }
@@ -47,13 +47,23 @@ export class SdCliCapacitor {
47
47
  }
48
48
  // 1. Capacitor 프로젝트 초기화
49
49
  async _initializeCapacitorProjectAsync(capacitorPath) {
50
- if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
50
+ const wwwPath = path.resolve(capacitorPath, "www");
51
+ if (FsUtils.exists(wwwPath)) {
51
52
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
52
53
  // 버전 동기화
53
- await this._syncVersionAsync(capacitorPath);
54
+ const pkgJsonPath = path.resolve(capacitorPath, "package.json");
55
+ if (FsUtils.exists(pkgJsonPath)) {
56
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
57
+ if (pkgJson.version !== this._npmConfig.version) {
58
+ pkgJson.version = this._npmConfig.version;
59
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
60
+ SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
61
+ }
62
+ }
54
63
  return false;
55
64
  }
56
- await FsUtils.mkdirsAsync(capacitorPath);
65
+ // www 폴더 생성
66
+ await FsUtils.mkdirsAsync(wwwPath);
57
67
  // package.json 생성
58
68
  const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
59
69
  const pkgJson = {
@@ -76,29 +86,17 @@ export class SdCliCapacitor {
76
86
  });
77
87
  // .yarnrc.yml 작성
78
88
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
79
- // yarn.lock 작성
89
+ // yarn.lock 작성
80
90
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
81
91
  // yarn install
92
+ await SdCliCapacitor._execAsync("yarn", ["dlx", "npm-check-updates", "-u", "--target", "semver"], capacitorPath);
82
93
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
83
94
  // capacitor init
84
95
  await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
85
96
  // www/index.html 생성
86
- const wwwPath = path.resolve(capacitorPath, "www");
87
97
  await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
88
98
  return true;
89
99
  }
90
- // 버전 동기화
91
- async _syncVersionAsync(capacitorPath) {
92
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
93
- if (FsUtils.exists(pkgJsonPath)) {
94
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
95
- if (pkgJson.version !== this._npmConfig.version) {
96
- pkgJson.version = this._npmConfig.version;
97
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
98
- SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
99
- }
100
- }
101
- }
102
100
  // 2. Capacitor 설정 파일 생성
103
101
  async _createCapacitorConfigAsync(capacitorPath) {
104
102
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
@@ -204,21 +202,14 @@ export class SdCliCapacitor {
204
202
  await FsUtils.removeAsync(keystorePath);
205
203
  }
206
204
  }
207
- // 6. 아이콘 및 스플래시 스크린 설정
208
205
  async _setupIconAndSplashScreenAsync(capacitorPath) {
209
- const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
206
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
210
207
  if (this._opt.config.icon != null) {
211
208
  await FsUtils.mkdirsAsync(resourcesDirPath);
212
209
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
213
- // logo.png: 1024x1024 (Easy Mode)
214
- // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
210
+ // 아이콘만 생성
215
211
  const logoPath = path.resolve(resourcesDirPath, "logo.png");
216
212
  await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
217
- // splash.png: 2732x2732
218
- // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
219
- const splashPath = path.resolve(resourcesDirPath, "splash.png");
220
- await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
221
- // 기존 아이콘 삭제 (겹침 방지)
222
213
  await this._cleanupExistingIconsAsync(capacitorPath);
223
214
  try {
224
215
  await SdCliCapacitor._execAsync("npx", [
@@ -231,8 +222,8 @@ export class SdCliCapacitor {
231
222
  "#ffffff",
232
223
  ], capacitorPath);
233
224
  }
234
- catch {
235
- SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
225
+ catch (e) {
226
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
236
227
  }
237
228
  }
238
229
  else {
@@ -262,13 +253,23 @@ export class SdCliCapacitor {
262
253
  const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
263
254
  if (!FsUtils.exists(androidResPath))
264
255
  return;
256
+ // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
265
257
  const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
266
258
  for (const dir of mipmapDirs) {
267
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
259
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
268
260
  for (const file of iconFiles) {
269
261
  await FsUtils.removeAsync(file);
270
262
  }
271
263
  }
264
+ // drawable 폴더의 splash/icon 관련 파일도 삭제
265
+ const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
266
+ for (const dir of drawableDirs) {
267
+ const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
268
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
269
+ for (const file of [...splashFiles, ...iconFiles]) {
270
+ await FsUtils.removeAsync(file);
271
+ }
272
+ }
272
273
  }
273
274
  // 7. Android 네이티브 설정
274
275
  async _configureAndroidNativeAsync(capacitorPath) {
@@ -295,10 +296,15 @@ export class SdCliCapacitor {
295
296
  return;
296
297
  }
297
298
  let stylesContent = await FsUtils.readFileAsync(stylesPath);
298
- // Edge-to-Edge 비활성화
299
+ // Edge-to-Edge 비활성화만
299
300
  if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
300
- stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
301
+ stylesContent = stylesContent.replace(/(<style[^>]*name="AppTheme"[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
301
302
  }
303
+ // NoActionBarLaunch를 단순 NoActionBar로
304
+ stylesContent = stylesContent.replace(/(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/, `$1Theme.AppCompat.Light.NoActionBar$2`);
305
+ // splash 관련 전부 제거
306
+ stylesContent = stylesContent.replace(/\s*<item name="android:background">@drawable\/splash<\/item>/g, "");
307
+ stylesContent = stylesContent.replace(/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g, "");
302
308
  await FsUtils.writeFileAsync(stylesPath, stylesContent);
303
309
  }
304
310
  async _configureAndroidGradlePropertiesAsync(androidPath) {
@@ -416,7 +422,7 @@ export class SdCliCapacitor {
416
422
  // Signing 설정
417
423
  const signConfig = this._opt.config.platform?.android?.sign;
418
424
  if (signConfig) {
419
- const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
425
+ const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
420
426
  const keystoreType = signConfig.keystoreType ?? "jks";
421
427
  // signingConfigs 블록 추가
422
428
  if (!gradleContent.includes("signingConfigs")) {
@@ -424,15 +430,15 @@ export class SdCliCapacitor {
424
430
  signingConfigs {
425
431
  release {
426
432
  storeFile file("${keystoreRelativePath}")
427
- storePassword "${signConfig.storePassword}"
428
- keyAlias "${signConfig.alias}"
429
- keyPassword "${signConfig.password}"
433
+ storePassword '${signConfig.storePassword}'
434
+ keyAlias '${signConfig.alias}'
435
+ keyPassword '${signConfig.password}'
430
436
  storeType "${keystoreType}"
431
437
  }
432
438
  }
433
439
  `;
434
440
  // android { 블록 내부에 추가
435
- gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
441
+ gradleContent = gradleContent.replace(/(android\s*\{)/, (match) => `${match}${signingConfigsBlock}`);
436
442
  }
437
443
  // buildTypes.release에 signingConfig 추가
438
444
  if (!gradleContent.includes("signingConfig signingConfigs.release")) {
@@ -460,34 +466,18 @@ export class SdCliCapacitor {
460
466
  await Promise.all(this._platforms.map(async (platform) => {
461
467
  // 해당 플랫폼만 copy
462
468
  await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
463
- await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
469
+ if (platform === "android") {
470
+ await this._buildAndroidAsync(capacitorPath, outPath, buildType);
471
+ }
464
472
  }));
465
473
  }
466
- async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
467
- if (platform === "android") {
468
- await this._buildAndroidAsync(capacitorPath, outPath, buildType);
469
- }
470
- // iOS 지원 시 추가
471
- }
472
474
  async _buildAndroidAsync(capacitorPath, outPath, buildType) {
473
475
  const androidPath = path.resolve(capacitorPath, "android");
474
476
  const targetOutPath = path.resolve(outPath, "android");
475
- // Gradle wrapper로 빌드
476
477
  const isBundle = this._opt.config.platform?.android?.bundle;
477
478
  const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
478
- // gradlew 실행 권한 부여 (Linux/Mac)
479
- const gradlewPath = path.resolve(androidPath, "gradlew");
480
- if (FsUtils.exists(gradlewPath)) {
481
- try {
482
- await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
483
- }
484
- catch {
485
- // Windows에서는 무시
486
- }
487
- }
488
479
  // Gradle 빌드 실행
489
- const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
490
- await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
480
+ await SdCliCapacitor._execAsync("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
491
481
  // 빌드 결과물 복사
492
482
  await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
493
483
  }
@@ -435,7 +435,7 @@ export class SdNgBundler {
435
435
  ...(this._conf.builderType === "electron"
436
436
  ? []
437
437
  : [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
438
- SdWorkerPathPlugin(path.resolve(this._opt.pkgPath, "dist")),
438
+ SdWorkerPathPlugin(this._outputPath),
439
439
  // {
440
440
  // name: "log-circular",
441
441
  // setup(build) {
@@ -24,7 +24,6 @@ export function SdWorkerPathPlugin(outdir) {
24
24
  return match;
25
25
  }
26
26
  // 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
27
- // 예: dist/workers/protocol.worker-X7A8B9.js
28
27
  const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
29
28
  const hash = HashUtils.get(fileContent).substring(0, 8);
30
29
  const workerBaseName = path.basename(resolvedWorkerPath, path.extname(resolvedWorkerPath));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.11",
3
+ "version": "12.16.13",
4
4
  "description": "심플리즘 패키지 - CLI",
5
5
  "author": "김석래",
6
6
  "repository": {
@@ -17,10 +17,10 @@
17
17
  "@angular/compiler-cli": "^20.3.15",
18
18
  "@anthropic-ai/sdk": "^0.71.2",
19
19
  "@electron/rebuild": "^4.0.2",
20
- "@simplysm/sd-core-common": "12.16.11",
21
- "@simplysm/sd-core-node": "12.16.11",
22
- "@simplysm/sd-service-server": "12.16.11",
23
- "@simplysm/sd-storage": "12.16.11",
20
+ "@simplysm/sd-core-common": "12.16.13",
21
+ "@simplysm/sd-core-node": "12.16.13",
22
+ "@simplysm/sd-service-server": "12.16.13",
23
+ "@simplysm/sd-storage": "12.16.13",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
@@ -10,7 +10,7 @@ export class SdCliCapacitor {
10
10
  private readonly _CAPACITOR_DIR_NAME = ".capacitor";
11
11
  private readonly _CONFIG_FILE_NAME = "capacitor.config.ts";
12
12
  private readonly _KEYSTORE_FILE_NAME = "android.keystore";
13
- private readonly _ICON_DIR_PATH = "resources";
13
+ private readonly _ICON_DIR_NAME = "resources";
14
14
 
15
15
  // private readonly _ANDROID_DIR_NAME = "android";
16
16
  // private readonly _WWW_DIR_NAME = "www";
@@ -67,16 +67,28 @@ export class SdCliCapacitor {
67
67
 
68
68
  // 1. Capacitor 프로젝트 초기화
69
69
  private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<boolean> {
70
- if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
70
+ const wwwPath = path.resolve(capacitorPath, "www");
71
+
72
+ if (FsUtils.exists(wwwPath)) {
71
73
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
72
74
 
73
75
  // 버전 동기화
74
- await this._syncVersionAsync(capacitorPath);
76
+ const pkgJsonPath = path.resolve(capacitorPath, "package.json");
77
+
78
+ if (FsUtils.exists(pkgJsonPath)) {
79
+ const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
80
+
81
+ if (pkgJson.version !== this._npmConfig.version) {
82
+ pkgJson.version = this._npmConfig.version;
83
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
84
+ SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
85
+ }
86
+ }
75
87
  return false;
76
88
  }
77
89
 
78
-
79
- await FsUtils.mkdirsAsync(capacitorPath);
90
+ // www 폴더 생성
91
+ await FsUtils.mkdirsAsync(wwwPath);
80
92
 
81
93
  // package.json 생성
82
94
  const projNpmConfig = await FsUtils.readJsonAsync(
@@ -110,10 +122,15 @@ export class SdCliCapacitor {
110
122
  "nodeLinker: node-modules",
111
123
  );
112
124
 
113
- // yarn.lock 작성
125
+ // yarn.lock 작성
114
126
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
115
127
 
116
128
  // yarn install
129
+ await SdCliCapacitor._execAsync(
130
+ "yarn",
131
+ ["dlx", "npm-check-updates", "-u", "--target", "semver"],
132
+ capacitorPath,
133
+ );
117
134
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
118
135
 
119
136
  // capacitor init
@@ -124,7 +141,6 @@ export class SdCliCapacitor {
124
141
  );
125
142
 
126
143
  // www/index.html 생성
127
- const wwwPath = path.resolve(capacitorPath, "www");
128
144
  await FsUtils.writeFileAsync(
129
145
  path.resolve(wwwPath, "index.html"),
130
146
  "<!DOCTYPE html><html><head></head><body></body></html>",
@@ -133,21 +149,6 @@ export class SdCliCapacitor {
133
149
  return true;
134
150
  }
135
151
 
136
- // 버전 동기화
137
- private async _syncVersionAsync(capacitorPath: string) {
138
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
139
-
140
- if (FsUtils.exists(pkgJsonPath)) {
141
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
142
-
143
- if (pkgJson.version !== this._npmConfig.version) {
144
- pkgJson.version = this._npmConfig.version;
145
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
146
- SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
147
- }
148
- }
149
- }
150
-
151
152
  // 2. Capacitor 설정 파일 생성
152
153
  private async _createCapacitorConfigAsync(capacitorPath: string) {
153
154
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
@@ -272,26 +273,18 @@ export class SdCliCapacitor {
272
273
  }
273
274
  }
274
275
 
275
- // 6. 아이콘 및 스플래시 스크린 설정
276
276
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
277
- const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
277
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
278
278
 
279
279
  if (this._opt.config.icon != null) {
280
280
  await FsUtils.mkdirsAsync(resourcesDirPath);
281
281
 
282
282
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
283
283
 
284
- // logo.png: 1024x1024 (Easy Mode)
285
- // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
284
+ // 아이콘만 생성
286
285
  const logoPath = path.resolve(resourcesDirPath, "logo.png");
287
286
  await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
288
287
 
289
- // splash.png: 2732x2732
290
- // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
291
- const splashPath = path.resolve(resourcesDirPath, "splash.png");
292
- await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
293
-
294
- // 기존 아이콘 삭제 (겹침 방지)
295
288
  await this._cleanupExistingIconsAsync(capacitorPath);
296
289
 
297
290
  try {
@@ -308,8 +301,8 @@ export class SdCliCapacitor {
308
301
  ],
309
302
  capacitorPath,
310
303
  );
311
- } catch {
312
- SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
304
+ } catch (e) {
305
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
313
306
  }
314
307
  } else {
315
308
  await FsUtils.removeAsync(resourcesDirPath);
@@ -347,13 +340,24 @@ export class SdCliCapacitor {
347
340
 
348
341
  if (!FsUtils.exists(androidResPath)) return;
349
342
 
343
+ // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
350
344
  const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
351
345
  for (const dir of mipmapDirs) {
352
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
346
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
353
347
  for (const file of iconFiles) {
354
348
  await FsUtils.removeAsync(file);
355
349
  }
356
350
  }
351
+
352
+ // drawable 폴더의 splash/icon 관련 파일도 삭제
353
+ const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
354
+ for (const dir of drawableDirs) {
355
+ const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
356
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
357
+ for (const file of [...splashFiles, ...iconFiles]) {
358
+ await FsUtils.removeAsync(file);
359
+ }
360
+ }
357
361
  }
358
362
 
359
363
  // 7. Android 네이티브 설정
@@ -392,14 +396,30 @@ export class SdCliCapacitor {
392
396
 
393
397
  let stylesContent = await FsUtils.readFileAsync(stylesPath);
394
398
 
395
- // Edge-to-Edge 비활성화
399
+ // Edge-to-Edge 비활성화만
396
400
  if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
397
401
  stylesContent = stylesContent.replace(
398
- /(<style[^>]*AppTheme[^>]*>)/,
402
+ /(<style[^>]*name="AppTheme"[^>]*>)/,
399
403
  `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
400
404
  );
401
405
  }
402
406
 
407
+ // NoActionBarLaunch를 단순 NoActionBar로
408
+ stylesContent = stylesContent.replace(
409
+ /(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/,
410
+ `$1Theme.AppCompat.Light.NoActionBar$2`,
411
+ );
412
+
413
+ // splash 관련 전부 제거
414
+ stylesContent = stylesContent.replace(
415
+ /\s*<item name="android:background">@drawable\/splash<\/item>/g,
416
+ "",
417
+ );
418
+ stylesContent = stylesContent.replace(
419
+ /\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
420
+ "",
421
+ );
422
+
403
423
  await FsUtils.writeFileAsync(stylesPath, stylesContent);
404
424
  }
405
425
 
@@ -560,7 +580,7 @@ export class SdCliCapacitor {
560
580
  // Signing 설정
561
581
  const signConfig = this._opt.config.platform?.android?.sign;
562
582
  if (signConfig) {
563
- const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
583
+ const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
564
584
  const keystoreType = signConfig.keystoreType ?? "jks";
565
585
 
566
586
  // signingConfigs 블록 추가
@@ -569,15 +589,18 @@ export class SdCliCapacitor {
569
589
  signingConfigs {
570
590
  release {
571
591
  storeFile file("${keystoreRelativePath}")
572
- storePassword "${signConfig.storePassword}"
573
- keyAlias "${signConfig.alias}"
574
- keyPassword "${signConfig.password}"
592
+ storePassword '${signConfig.storePassword}'
593
+ keyAlias '${signConfig.alias}'
594
+ keyPassword '${signConfig.password}'
575
595
  storeType "${keystoreType}"
576
596
  }
577
597
  }
578
598
  `;
579
599
  // android { 블록 내부에 추가
580
- gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
600
+ gradleContent = gradleContent.replace(
601
+ /(android\s*\{)/,
602
+ (match) => `${match}${signingConfigsBlock}`,
603
+ );
581
604
  }
582
605
 
583
606
  // buildTypes.release에 signingConfig 추가
@@ -629,23 +652,14 @@ export class SdCliCapacitor {
629
652
  this._platforms.map(async (platform) => {
630
653
  // 해당 플랫폼만 copy
631
654
  await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
632
- await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
655
+
656
+ if (platform === "android") {
657
+ await this._buildAndroidAsync(capacitorPath, outPath, buildType);
658
+ }
633
659
  }),
634
660
  );
635
661
  }
636
662
 
637
- private async _buildPlatformAsync(
638
- capacitorPath: string,
639
- outPath: string,
640
- platform: string,
641
- buildType: string,
642
- ): Promise<void> {
643
- if (platform === "android") {
644
- await this._buildAndroidAsync(capacitorPath, outPath, buildType);
645
- }
646
- // iOS 지원 시 추가
647
- }
648
-
649
663
  private async _buildAndroidAsync(
650
664
  capacitorPath: string,
651
665
  outPath: string,
@@ -654,24 +668,16 @@ export class SdCliCapacitor {
654
668
  const androidPath = path.resolve(capacitorPath, "android");
655
669
  const targetOutPath = path.resolve(outPath, "android");
656
670
 
657
- // Gradle wrapper로 빌드
658
671
  const isBundle = this._opt.config.platform?.android?.bundle;
659
672
  const gradleTask =
660
673
  buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
661
674
 
662
- // gradlew 실행 권한 부여 (Linux/Mac)
663
- const gradlewPath = path.resolve(androidPath, "gradlew");
664
- if (FsUtils.exists(gradlewPath)) {
665
- try {
666
- await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
667
- } catch {
668
- // Windows에서는 무시
669
- }
670
- }
671
-
672
675
  // Gradle 빌드 실행
673
- const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
674
- await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
676
+ await SdCliCapacitor._execAsync(
677
+ "cmd",
678
+ ["/c", "gradlew.bat", gradleTask, "--no-daemon"],
679
+ androidPath,
680
+ );
675
681
 
676
682
  // 빌드 결과물 복사
677
683
  await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
@@ -560,7 +560,7 @@ export class SdNgBundler {
560
560
  ...(this._conf.builderType === "electron"
561
561
  ? []
562
562
  : [nodeStdLibBrowserPlugin(nodeStdLibBrowser)]),
563
- SdWorkerPathPlugin(path.resolve(this._opt.pkgPath, "dist")),
563
+ SdWorkerPathPlugin(this._outputPath),
564
564
  // {
565
565
  // name: "log-circular",
566
566
  // setup(build) {
@@ -33,7 +33,6 @@ export function SdWorkerPathPlugin(outdir: string): esbuild.Plugin {
33
33
  }
34
34
 
35
35
  // 2. 출력될 워커 파일명 결정 (캐싱 및 중복 방지를 위해 해시 사용 권장)
36
- // 예: dist/workers/protocol.worker-X7A8B9.js
37
36
  const fileContent = await FsUtils.readFileBufferAsync(resolvedWorkerPath);
38
37
  const hash = HashUtils.get(fileContent).substring(0, 8);
39
38
  const workerBaseName = path.basename(