@simplysm/sd-cli 12.16.12 → 12.16.14

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,11 +15,9 @@ 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;
22
- private _isCapacitorPlugin;
23
21
  private _setupAndroidSignAsync;
24
22
  private _setupIconAndSplashScreenAsync;
25
23
  private _createCenteredImageAsync;
@@ -33,7 +31,6 @@ export declare class SdCliCapacitor {
33
31
  private _configureAndroidBuildGradleAsync;
34
32
  private _configureAndroidStringsAsync;
35
33
  buildAsync(outPath: string): Promise<void>;
36
- private _buildPlatformAsync;
37
34
  private _buildAndroidAsync;
38
35
  private _copyAndroidBuildOutputAsync;
39
36
  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 = {
@@ -64,12 +74,11 @@ export class SdCliCapacitor {
64
74
  dependencies: {
65
75
  "@capacitor/core": "^7.0.0",
66
76
  "@capacitor/app": "^7.0.0",
67
- "@capacitor/splash-screen": "^7.0.0",
77
+ ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
68
78
  },
69
79
  devDependencies: {
70
80
  "@capacitor/cli": "^7.0.0",
71
81
  "@capacitor/assets": "^3.0.0",
72
- ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
73
82
  },
74
83
  };
75
84
  await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
@@ -77,29 +86,17 @@ export class SdCliCapacitor {
77
86
  });
78
87
  // .yarnrc.yml 작성
79
88
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
80
- // yarn.lock 작성
89
+ // yarn.lock 작성
81
90
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
82
91
  // yarn install
92
+ await SdCliCapacitor._execAsync("yarn", ["dlx", "npm-check-updates", "-u", "--target", "semver"], capacitorPath);
83
93
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
84
94
  // capacitor init
85
95
  await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
86
96
  // www/index.html 생성
87
- const wwwPath = path.resolve(capacitorPath, "www");
88
97
  await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
89
98
  return true;
90
99
  }
91
- // 버전 동기화
92
- async _syncVersionAsync(capacitorPath) {
93
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
94
- if (FsUtils.exists(pkgJsonPath)) {
95
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
96
- if (pkgJson.version !== this._npmConfig.version) {
97
- pkgJson.version = this._npmConfig.version;
98
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
99
- SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
100
- }
101
- }
102
- }
103
100
  // 2. Capacitor 설정 파일 생성
104
101
  async _createCapacitorConfigAsync(capacitorPath) {
105
102
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
@@ -156,18 +153,24 @@ export class SdCliCapacitor {
156
153
  const usePlugins = Object.keys(this._opt.config.plugins ?? {});
157
154
  const currentDeps = pkgJson.dependencies ?? {};
158
155
  let changed = false;
156
+ const prevPlugins = Object.keys(currentDeps).filter(item => ![
157
+ "@capacitor/core",
158
+ "@capacitor/android",
159
+ "@capacitor/ios",
160
+ "@capacitor/app",
161
+ ].includes(item));
159
162
  // 사용하지 않는 플러그인 제거
160
- for (const dep of Object.keys(currentDeps)) {
161
- if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
162
- delete currentDeps[dep];
163
+ for (const prevPlugin of prevPlugins) {
164
+ if (!usePlugins.includes(prevPlugin)) {
165
+ delete currentDeps[prevPlugin];
163
166
  changed = true;
164
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
167
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
165
168
  }
166
169
  }
167
170
  // 새 플러그인 추가
168
171
  for (const plugin of usePlugins) {
169
172
  if (!(plugin in currentDeps)) {
170
- const version = mainDeps[plugin] ?? "^7.0.0";
173
+ const version = mainDeps[plugin] ?? "*";
171
174
  currentDeps[plugin] = version;
172
175
  changed = true;
173
176
  SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
@@ -183,18 +186,6 @@ export class SdCliCapacitor {
183
186
  // 변경 없으면 아무것도 안 함 → 오프라인 OK
184
187
  return false;
185
188
  }
186
- _isCapacitorPlugin(dep) {
187
- // 기본 패키지 제외
188
- const corePackages = [
189
- "@capacitor/core",
190
- "@capacitor/android",
191
- "@capacitor/ios",
192
- "@capacitor/app",
193
- ];
194
- if (corePackages.includes(dep))
195
- return false;
196
- return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
197
- }
198
189
  // 5. 안드로이드 서명 설정
199
190
  async _setupAndroidSignAsync(capacitorPath) {
200
191
  const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
@@ -205,21 +196,14 @@ export class SdCliCapacitor {
205
196
  await FsUtils.removeAsync(keystorePath);
206
197
  }
207
198
  }
208
- // 6. 아이콘 및 스플래시 스크린 설정
209
199
  async _setupIconAndSplashScreenAsync(capacitorPath) {
210
- const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
200
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
211
201
  if (this._opt.config.icon != null) {
212
202
  await FsUtils.mkdirsAsync(resourcesDirPath);
213
203
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
214
- // logo.png: 1024x1024 (Easy Mode)
215
- // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
204
+ // 아이콘만 생성
216
205
  const logoPath = path.resolve(resourcesDirPath, "logo.png");
217
206
  await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
218
- // splash.png: 2732x2732
219
- // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
220
- const splashPath = path.resolve(resourcesDirPath, "splash.png");
221
- await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
222
- // 기존 아이콘 삭제 (겹침 방지)
223
207
  await this._cleanupExistingIconsAsync(capacitorPath);
224
208
  try {
225
209
  await SdCliCapacitor._execAsync("npx", [
@@ -232,8 +216,8 @@ export class SdCliCapacitor {
232
216
  "#ffffff",
233
217
  ], capacitorPath);
234
218
  }
235
- catch {
236
- SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
219
+ catch (e) {
220
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
237
221
  }
238
222
  }
239
223
  else {
@@ -263,13 +247,23 @@ export class SdCliCapacitor {
263
247
  const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
264
248
  if (!FsUtils.exists(androidResPath))
265
249
  return;
250
+ // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
266
251
  const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
267
252
  for (const dir of mipmapDirs) {
268
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
253
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
269
254
  for (const file of iconFiles) {
270
255
  await FsUtils.removeAsync(file);
271
256
  }
272
257
  }
258
+ // drawable 폴더의 splash/icon 관련 파일도 삭제
259
+ const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
260
+ for (const dir of drawableDirs) {
261
+ const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
262
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
263
+ for (const file of [...splashFiles, ...iconFiles]) {
264
+ await FsUtils.removeAsync(file);
265
+ }
266
+ }
273
267
  }
274
268
  // 7. Android 네이티브 설정
275
269
  async _configureAndroidNativeAsync(capacitorPath) {
@@ -296,10 +290,15 @@ export class SdCliCapacitor {
296
290
  return;
297
291
  }
298
292
  let stylesContent = await FsUtils.readFileAsync(stylesPath);
299
- // Edge-to-Edge 비활성화
293
+ // Edge-to-Edge 비활성화만
300
294
  if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
301
- stylesContent = stylesContent.replace(/(<style[^>]*AppTheme[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
295
+ stylesContent = stylesContent.replace(/(<style[^>]*name="AppTheme"[^>]*>)/, `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`);
302
296
  }
297
+ // NoActionBarLaunch를 단순 NoActionBar로
298
+ stylesContent = stylesContent.replace(/(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/, `$1Theme.AppCompat.Light.NoActionBar$2`);
299
+ // splash 관련 전부 제거
300
+ stylesContent = stylesContent.replace(/\s*<item name="android:background">@drawable\/splash<\/item>/g, "");
301
+ stylesContent = stylesContent.replace(/\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g, "");
303
302
  await FsUtils.writeFileAsync(stylesPath, stylesContent);
304
303
  }
305
304
  async _configureAndroidGradlePropertiesAsync(androidPath) {
@@ -417,7 +416,7 @@ export class SdCliCapacitor {
417
416
  // Signing 설정
418
417
  const signConfig = this._opt.config.platform?.android?.sign;
419
418
  if (signConfig) {
420
- const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
419
+ const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
421
420
  const keystoreType = signConfig.keystoreType ?? "jks";
422
421
  // signingConfigs 블록 추가
423
422
  if (!gradleContent.includes("signingConfigs")) {
@@ -425,15 +424,15 @@ export class SdCliCapacitor {
425
424
  signingConfigs {
426
425
  release {
427
426
  storeFile file("${keystoreRelativePath}")
428
- storePassword "${signConfig.storePassword}"
429
- keyAlias "${signConfig.alias}"
430
- keyPassword "${signConfig.password}"
427
+ storePassword '${signConfig.storePassword}'
428
+ keyAlias '${signConfig.alias}'
429
+ keyPassword '${signConfig.password}'
431
430
  storeType "${keystoreType}"
432
431
  }
433
432
  }
434
433
  `;
435
434
  // android { 블록 내부에 추가
436
- gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
435
+ gradleContent = gradleContent.replace(/(android\s*\{)/, (match) => `${match}${signingConfigsBlock}`);
437
436
  }
438
437
  // buildTypes.release에 signingConfig 추가
439
438
  if (!gradleContent.includes("signingConfig signingConfigs.release")) {
@@ -461,34 +460,18 @@ export class SdCliCapacitor {
461
460
  await Promise.all(this._platforms.map(async (platform) => {
462
461
  // 해당 플랫폼만 copy
463
462
  await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
464
- await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
463
+ if (platform === "android") {
464
+ await this._buildAndroidAsync(capacitorPath, outPath, buildType);
465
+ }
465
466
  }));
466
467
  }
467
- async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
468
- if (platform === "android") {
469
- await this._buildAndroidAsync(capacitorPath, outPath, buildType);
470
- }
471
- // iOS 지원 시 추가
472
- }
473
468
  async _buildAndroidAsync(capacitorPath, outPath, buildType) {
474
469
  const androidPath = path.resolve(capacitorPath, "android");
475
470
  const targetOutPath = path.resolve(outPath, "android");
476
- // Gradle wrapper로 빌드
477
471
  const isBundle = this._opt.config.platform?.android?.bundle;
478
472
  const gradleTask = buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
479
- // gradlew 실행 권한 부여 (Linux/Mac)
480
- const gradlewPath = path.resolve(androidPath, "gradlew");
481
- if (FsUtils.exists(gradlewPath)) {
482
- try {
483
- await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
484
- }
485
- catch {
486
- // Windows에서는 무시
487
- }
488
- }
489
473
  // Gradle 빌드 실행
490
- const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
491
- await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
474
+ await SdCliCapacitor._execAsync("cmd", ["/c", "gradlew.bat", gradleTask, "--no-daemon"], androidPath);
492
475
  // 빌드 결과물 복사
493
476
  await this._copyAndroidBuildOutputAsync(androidPath, targetOutPath, buildType);
494
477
  }
@@ -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.12",
3
+ "version": "12.16.14",
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.12",
21
- "@simplysm/sd-core-node": "12.16.12",
22
- "@simplysm/sd-service-server": "12.16.12",
23
- "@simplysm/sd-storage": "12.16.12",
20
+ "@simplysm/sd-core-common": "12.16.14",
21
+ "@simplysm/sd-core-node": "12.16.14",
22
+ "@simplysm/sd-service-server": "12.16.14",
23
+ "@simplysm/sd-storage": "12.16.14",
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(
@@ -90,16 +102,15 @@ export class SdCliCapacitor {
90
102
  dependencies: {
91
103
  "@capacitor/core": "^7.0.0",
92
104
  "@capacitor/app": "^7.0.0",
93
- "@capacitor/splash-screen": "^7.0.0",
94
- },
95
- devDependencies: {
96
- "@capacitor/cli": "^7.0.0",
97
- "@capacitor/assets": "^3.0.0",
98
105
  ...this._platforms.toObject(
99
106
  (item) => `@capacitor/${item}`,
100
107
  () => "^7.0.0",
101
108
  ),
102
109
  },
110
+ devDependencies: {
111
+ "@capacitor/cli": "^7.0.0",
112
+ "@capacitor/assets": "^3.0.0",
113
+ },
103
114
  };
104
115
  await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
105
116
  space: 2,
@@ -111,10 +122,15 @@ export class SdCliCapacitor {
111
122
  "nodeLinker: node-modules",
112
123
  );
113
124
 
114
- // yarn.lock 작성
125
+ // yarn.lock 작성
115
126
  await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
116
127
 
117
128
  // yarn install
129
+ await SdCliCapacitor._execAsync(
130
+ "yarn",
131
+ ["dlx", "npm-check-updates", "-u", "--target", "semver"],
132
+ capacitorPath,
133
+ );
118
134
  await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
119
135
 
120
136
  // capacitor init
@@ -125,7 +141,6 @@ export class SdCliCapacitor {
125
141
  );
126
142
 
127
143
  // www/index.html 생성
128
- const wwwPath = path.resolve(capacitorPath, "www");
129
144
  await FsUtils.writeFileAsync(
130
145
  path.resolve(wwwPath, "index.html"),
131
146
  "<!DOCTYPE html><html><head></head><body></body></html>",
@@ -134,21 +149,6 @@ export class SdCliCapacitor {
134
149
  return true;
135
150
  }
136
151
 
137
- // 버전 동기화
138
- private async _syncVersionAsync(capacitorPath: string) {
139
- const pkgJsonPath = path.resolve(capacitorPath, "package.json");
140
-
141
- if (FsUtils.exists(pkgJsonPath)) {
142
- const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
143
-
144
- if (pkgJson.version !== this._npmConfig.version) {
145
- pkgJson.version = this._npmConfig.version;
146
- await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
147
- SdCliCapacitor._logger.debug(`버전 동기화: ${this._npmConfig.version}`);
148
- }
149
- }
150
- }
151
-
152
152
  // 2. Capacitor 설정 파일 생성
153
153
  private async _createCapacitorConfigAsync(capacitorPath: string) {
154
154
  const configFilePath = path.resolve(capacitorPath, this._CONFIG_FILE_NAME);
@@ -216,19 +216,26 @@ export class SdCliCapacitor {
216
216
 
217
217
  let changed = false;
218
218
 
219
+ const prevPlugins = Object.keys(currentDeps).filter(item => ![
220
+ "@capacitor/core",
221
+ "@capacitor/android",
222
+ "@capacitor/ios",
223
+ "@capacitor/app",
224
+ ].includes(item));
225
+
219
226
  // 사용하지 않는 플러그인 제거
220
- for (const dep of Object.keys(currentDeps)) {
221
- if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
222
- delete currentDeps[dep];
227
+ for (const prevPlugin of prevPlugins) {
228
+ if (!usePlugins.includes(prevPlugin)) {
229
+ delete currentDeps[prevPlugin];
223
230
  changed = true;
224
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
231
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${prevPlugin}`);
225
232
  }
226
233
  }
227
234
 
228
235
  // 새 플러그인 추가
229
236
  for (const plugin of usePlugins) {
230
237
  if (!(plugin in currentDeps)) {
231
- const version = mainDeps[plugin] ?? "^7.0.0";
238
+ const version = mainDeps[plugin] ?? "*";
232
239
  currentDeps[plugin] = version;
233
240
  changed = true;
234
241
  SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
@@ -246,19 +253,6 @@ export class SdCliCapacitor {
246
253
  return false;
247
254
  }
248
255
 
249
- private _isCapacitorPlugin(dep: string): boolean {
250
- // 기본 패키지 제외
251
- const corePackages = [
252
- "@capacitor/core",
253
- "@capacitor/android",
254
- "@capacitor/ios",
255
- "@capacitor/app",
256
- ];
257
- if (corePackages.includes(dep)) return false;
258
-
259
- return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
260
- }
261
-
262
256
  // 5. 안드로이드 서명 설정
263
257
  private async _setupAndroidSignAsync(capacitorPath: string) {
264
258
  const keystorePath = path.resolve(capacitorPath, this._KEYSTORE_FILE_NAME);
@@ -273,26 +267,18 @@ export class SdCliCapacitor {
273
267
  }
274
268
  }
275
269
 
276
- // 6. 아이콘 및 스플래시 스크린 설정
277
270
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
278
- const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
271
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_NAME);
279
272
 
280
273
  if (this._opt.config.icon != null) {
281
274
  await FsUtils.mkdirsAsync(resourcesDirPath);
282
275
 
283
276
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
284
277
 
285
- // logo.png: 1024x1024 (Easy Mode)
286
- // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
278
+ // 아이콘만 생성
287
279
  const logoPath = path.resolve(resourcesDirPath, "logo.png");
288
280
  await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
289
281
 
290
- // splash.png: 2732x2732
291
- // 로고 크기 약 35% - 화면 중앙에 적당한 크기로
292
- const splashPath = path.resolve(resourcesDirPath, "splash.png");
293
- await this._createCenteredImageAsync(iconSource, splashPath, 2732, 0.35);
294
-
295
- // 기존 아이콘 삭제 (겹침 방지)
296
282
  await this._cleanupExistingIconsAsync(capacitorPath);
297
283
 
298
284
  try {
@@ -309,8 +295,8 @@ export class SdCliCapacitor {
309
295
  ],
310
296
  capacitorPath,
311
297
  );
312
- } catch {
313
- SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
298
+ } catch (e) {
299
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용", e);
314
300
  }
315
301
  } else {
316
302
  await FsUtils.removeAsync(resourcesDirPath);
@@ -348,13 +334,24 @@ export class SdCliCapacitor {
348
334
 
349
335
  if (!FsUtils.exists(androidResPath)) return;
350
336
 
337
+ // mipmap 폴더의 모든 ic_launcher 관련 파일 삭제 (png + xml)
351
338
  const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
352
339
  for (const dir of mipmapDirs) {
353
- const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
340
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*")); // 확장자 제거
354
341
  for (const file of iconFiles) {
355
342
  await FsUtils.removeAsync(file);
356
343
  }
357
344
  }
345
+
346
+ // drawable 폴더의 splash/icon 관련 파일도 삭제
347
+ const drawableDirs = await FsUtils.globAsync(path.resolve(androidResPath, "drawable*"));
348
+ for (const dir of drawableDirs) {
349
+ const splashFiles = await FsUtils.globAsync(path.resolve(dir, "splash*"));
350
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*"));
351
+ for (const file of [...splashFiles, ...iconFiles]) {
352
+ await FsUtils.removeAsync(file);
353
+ }
354
+ }
358
355
  }
359
356
 
360
357
  // 7. Android 네이티브 설정
@@ -393,14 +390,30 @@ export class SdCliCapacitor {
393
390
 
394
391
  let stylesContent = await FsUtils.readFileAsync(stylesPath);
395
392
 
396
- // Edge-to-Edge 비활성화
393
+ // Edge-to-Edge 비활성화만
397
394
  if (!stylesContent.includes("android:windowOptOutEdgeToEdgeEnforcement")) {
398
395
  stylesContent = stylesContent.replace(
399
- /(<style[^>]*AppTheme[^>]*>)/,
396
+ /(<style[^>]*name="AppTheme"[^>]*>)/,
400
397
  `$1\n <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>`,
401
398
  );
402
399
  }
403
400
 
401
+ // NoActionBarLaunch를 단순 NoActionBar로
402
+ stylesContent = stylesContent.replace(
403
+ /(<style\s+name="AppTheme\.NoActionBarLaunch"\s+parent=")[^"]+(")/,
404
+ `$1Theme.AppCompat.Light.NoActionBar$2`,
405
+ );
406
+
407
+ // splash 관련 전부 제거
408
+ stylesContent = stylesContent.replace(
409
+ /\s*<item name="android:background">@drawable\/splash<\/item>/g,
410
+ "",
411
+ );
412
+ stylesContent = stylesContent.replace(
413
+ /\s*<item name="android:windowSplashScreen[^"]*">[^<]*<\/item>/g,
414
+ "",
415
+ );
416
+
404
417
  await FsUtils.writeFileAsync(stylesPath, stylesContent);
405
418
  }
406
419
 
@@ -561,7 +574,7 @@ export class SdCliCapacitor {
561
574
  // Signing 설정
562
575
  const signConfig = this._opt.config.platform?.android?.sign;
563
576
  if (signConfig) {
564
- const keystoreRelativePath = `../${this._KEYSTORE_FILE_NAME}`;
577
+ const keystoreRelativePath = `../../${this._KEYSTORE_FILE_NAME}`;
565
578
  const keystoreType = signConfig.keystoreType ?? "jks";
566
579
 
567
580
  // signingConfigs 블록 추가
@@ -570,15 +583,18 @@ export class SdCliCapacitor {
570
583
  signingConfigs {
571
584
  release {
572
585
  storeFile file("${keystoreRelativePath}")
573
- storePassword "${signConfig.storePassword}"
574
- keyAlias "${signConfig.alias}"
575
- keyPassword "${signConfig.password}"
586
+ storePassword '${signConfig.storePassword}'
587
+ keyAlias '${signConfig.alias}'
588
+ keyPassword '${signConfig.password}'
576
589
  storeType "${keystoreType}"
577
590
  }
578
591
  }
579
592
  `;
580
593
  // android { 블록 내부에 추가
581
- gradleContent = gradleContent.replace(/(android\s*\{)/, `$1${signingConfigsBlock}`);
594
+ gradleContent = gradleContent.replace(
595
+ /(android\s*\{)/,
596
+ (match) => `${match}${signingConfigsBlock}`,
597
+ );
582
598
  }
583
599
 
584
600
  // buildTypes.release에 signingConfig 추가
@@ -630,23 +646,14 @@ export class SdCliCapacitor {
630
646
  this._platforms.map(async (platform) => {
631
647
  // 해당 플랫폼만 copy
632
648
  await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
633
- await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
649
+
650
+ if (platform === "android") {
651
+ await this._buildAndroidAsync(capacitorPath, outPath, buildType);
652
+ }
634
653
  }),
635
654
  );
636
655
  }
637
656
 
638
- private async _buildPlatformAsync(
639
- capacitorPath: string,
640
- outPath: string,
641
- platform: string,
642
- buildType: string,
643
- ): Promise<void> {
644
- if (platform === "android") {
645
- await this._buildAndroidAsync(capacitorPath, outPath, buildType);
646
- }
647
- // iOS 지원 시 추가
648
- }
649
-
650
657
  private async _buildAndroidAsync(
651
658
  capacitorPath: string,
652
659
  outPath: string,
@@ -655,24 +662,16 @@ export class SdCliCapacitor {
655
662
  const androidPath = path.resolve(capacitorPath, "android");
656
663
  const targetOutPath = path.resolve(outPath, "android");
657
664
 
658
- // Gradle wrapper로 빌드
659
665
  const isBundle = this._opt.config.platform?.android?.bundle;
660
666
  const gradleTask =
661
667
  buildType === "release" ? (isBundle ? "bundleRelease" : "assembleRelease") : "assembleDebug";
662
668
 
663
- // gradlew 실행 권한 부여 (Linux/Mac)
664
- const gradlewPath = path.resolve(androidPath, "gradlew");
665
- if (FsUtils.exists(gradlewPath)) {
666
- try {
667
- await SdCliCapacitor._execAsync("chmod", ["+x", "gradlew"], androidPath);
668
- } catch {
669
- // Windows에서는 무시
670
- }
671
- }
672
-
673
669
  // Gradle 빌드 실행
674
- const gradleCmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
675
- await SdCliCapacitor._execAsync(gradleCmd, [gradleTask, "--no-daemon"], androidPath);
670
+ await SdCliCapacitor._execAsync(
671
+ "cmd",
672
+ ["/c", "gradlew.bat", gradleTask, "--no-daemon"],
673
+ androidPath,
674
+ );
676
675
 
677
676
  // 빌드 결과물 복사
678
677
  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(