@simplysm/sd-cli 12.16.8 → 12.16.10

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.
@@ -19,9 +19,11 @@ export declare class SdCliCapacitor {
19
19
  private _createCapacitorConfigAsync;
20
20
  private _managePlatformsAsync;
21
21
  private _managePluginsAsync;
22
+ private _isCapacitorPlugin;
22
23
  private _setupAndroidSignAsync;
23
24
  private _setupIconAndSplashScreenAsync;
24
- private _createPaddedIconAsync;
25
+ private _createCenteredImageAsync;
26
+ private _cleanupExistingIconsAsync;
25
27
  private _configureAndroidNativeAsync;
26
28
  private _configureAndroidStylesAsync;
27
29
  private _configureAndroidGradlePropertiesAsync;
@@ -22,13 +22,13 @@ export class SdCliCapacitor {
22
22
  async initializeAsync() {
23
23
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
24
24
  // 1. Capacitor 프로젝트 초기화
25
- await this._initializeCapacitorProjectAsync(capacitorPath);
25
+ const isNewProject = await this._initializeCapacitorProjectAsync(capacitorPath);
26
26
  // 2. Capacitor 설정 파일 생성
27
27
  await this._createCapacitorConfigAsync(capacitorPath);
28
28
  // 3. 플랫폼 관리
29
29
  await this._managePlatformsAsync(capacitorPath);
30
30
  // 4. 플러그인 관리
31
- await this._managePluginsAsync(capacitorPath);
31
+ const pluginsChanged = await this._managePluginsAsync(capacitorPath);
32
32
  // 5. 안드로이드 서명 설정
33
33
  await this._setupAndroidSignAsync(capacitorPath);
34
34
  // 6. 아이콘 및 스플래시 스크린 설정
@@ -38,7 +38,12 @@ export class SdCliCapacitor {
38
38
  await this._configureAndroidNativeAsync(capacitorPath);
39
39
  }
40
40
  // 8. 웹 자산 동기화
41
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
41
+ if (isNewProject || pluginsChanged) {
42
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
43
+ }
44
+ else {
45
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy"], capacitorPath);
46
+ }
42
47
  }
43
48
  // 1. Capacitor 프로젝트 초기화
44
49
  async _initializeCapacitorProjectAsync(capacitorPath) {
@@ -46,41 +51,41 @@ export class SdCliCapacitor {
46
51
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
47
52
  // 버전 동기화
48
53
  await this._syncVersionAsync(capacitorPath);
49
- }
50
- else {
51
- await FsUtils.mkdirsAsync(capacitorPath);
52
- // package.json 생성
53
- const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
54
- const pkgJson = {
55
- name: this._opt.config.appId,
56
- version: this._npmConfig.version,
57
- private: true,
58
- volta: projNpmConfig.volta,
59
- dependencies: {
60
- "@capacitor/core": "^7.0.0",
61
- "@capacitor/app": "^7.0.0",
62
- },
63
- devDependencies: {
64
- "@capacitor/cli": "^7.0.0",
65
- "@capacitor/assets": "^3.0.0",
66
- ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
67
- },
68
- };
69
- await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
70
- space: 2,
71
- });
72
- // .yarnrc.yml 작성
73
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
74
- // yarn.lock 작성
75
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
76
- // yarn install
77
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
78
- // capacitor init
79
- await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
80
- }
54
+ return false;
55
+ }
56
+ await FsUtils.mkdirsAsync(capacitorPath);
57
+ // package.json 생성
58
+ const projNpmConfig = await FsUtils.readJsonAsync(path.resolve(this._opt.pkgPath, "../../package.json"));
59
+ const pkgJson = {
60
+ name: this._opt.config.appId,
61
+ version: this._npmConfig.version,
62
+ private: true,
63
+ volta: projNpmConfig.volta,
64
+ dependencies: {
65
+ "@capacitor/core": "^7.0.0",
66
+ "@capacitor/app": "^7.0.0",
67
+ },
68
+ devDependencies: {
69
+ "@capacitor/cli": "^7.0.0",
70
+ "@capacitor/assets": "^3.0.0",
71
+ ...this._platforms.toObject((item) => `@capacitor/${item}`, () => "^7.0.0"),
72
+ },
73
+ };
74
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
75
+ space: 2,
76
+ });
77
+ // .yarnrc.yml 작성
78
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, ".yarnrc.yml"), "nodeLinker: node-modules");
79
+ // yarn.lock 작성
80
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
81
+ // yarn install
82
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
83
+ // capacitor init
84
+ await SdCliCapacitor._execAsync("npx", ["cap", "init", this._opt.config.appName, this._opt.config.appId], capacitorPath);
81
85
  // www/index.html 생성
82
86
  const wwwPath = path.resolve(capacitorPath, "www");
83
87
  await FsUtils.writeFileAsync(path.resolve(wwwPath, "index.html"), "<!DOCTYPE html><html><head></head><body></body></html>");
88
+ return true;
84
89
  }
85
90
  // 버전 동기화
86
91
  async _syncVersionAsync(capacitorPath) {
@@ -139,43 +144,55 @@ export class SdCliCapacitor {
139
144
  await SdCliCapacitor._execAsync("npx", ["cap", "add", platform], capacitorPath);
140
145
  }
141
146
  }
142
- // 4. 플러그인 관리
143
147
  async _managePluginsAsync(capacitorPath) {
144
148
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
145
149
  const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath));
146
- const currentDeps = Object.keys(pkgJson.dependencies ?? {});
147
- const usePlugins = Object.keys(this._opt.config.plugins ?? {});
148
- // 사용하지 않는 플러그인 제거
149
- for (const dep of currentDeps) {
150
- // @capacitor/core, @capacitor/android 등 기본 패키지는 제외
151
- if (dep.startsWith("@capacitor/") &&
152
- ["core", "android", "ios"].some((p) => dep.endsWith(p))) {
153
- continue;
154
- }
155
- // 플러그인 목록에 없는 패키지는 제거
156
- if (!usePlugins.includes(dep)) {
157
- // Capacitor 관련 플러그인만 제거
158
- if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
159
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
160
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
161
- }
162
- }
163
- }
164
- // 새 플러그인 설치
165
150
  const mainDeps = {
166
151
  ...this._npmConfig.dependencies,
167
152
  ...this._npmConfig.devDependencies,
168
153
  ...this._npmConfig.peerDependencies,
169
154
  };
155
+ const usePlugins = Object.keys(this._opt.config.plugins ?? {});
156
+ const currentDeps = pkgJson.dependencies ?? {};
157
+ let changed = false;
158
+ // 사용하지 않는 플러그인 제거
159
+ for (const dep of Object.keys(currentDeps)) {
160
+ if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
161
+ delete currentDeps[dep];
162
+ changed = true;
163
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
164
+ }
165
+ }
166
+ // 새 플러그인 추가
170
167
  for (const plugin of usePlugins) {
171
- if (!currentDeps.includes(plugin)) {
172
- // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
173
- const version = mainDeps[plugin];
174
- const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
175
- await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
176
- SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
168
+ if (!(plugin in currentDeps)) {
169
+ const version = mainDeps[plugin] ?? "^7.0.0";
170
+ currentDeps[plugin] = version;
171
+ changed = true;
172
+ SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
177
173
  }
178
174
  }
175
+ // 변경사항 있을 때만 저장 & install
176
+ if (changed) {
177
+ pkgJson.dependencies = currentDeps;
178
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
179
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
180
+ return true;
181
+ }
182
+ // 변경 없으면 아무것도 안 함 → 오프라인 OK
183
+ return false;
184
+ }
185
+ _isCapacitorPlugin(dep) {
186
+ // 기본 패키지 제외
187
+ const corePackages = [
188
+ "@capacitor/core",
189
+ "@capacitor/android",
190
+ "@capacitor/ios",
191
+ "@capacitor/app",
192
+ ];
193
+ if (corePackages.includes(dep))
194
+ return false;
195
+ return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
179
196
  }
180
197
  // 5. 안드로이드 서명 설정
181
198
  async _setupAndroidSignAsync(capacitorPath) {
@@ -189,33 +206,45 @@ export class SdCliCapacitor {
189
206
  }
190
207
  // 6. 아이콘 및 스플래시 스크린 설정
191
208
  async _setupIconAndSplashScreenAsync(capacitorPath) {
192
- const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
209
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
193
210
  if (this._opt.config.icon != null) {
194
- await FsUtils.mkdirsAsync(iconDirPath);
211
+ await FsUtils.mkdirsAsync(resourcesDirPath);
195
212
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
196
- // icon.png 자체를 여백 포함으로 생성
197
- const iconPath = path.resolve(iconDirPath, "icon.png");
198
- await this._createPaddedIconAsync(iconSource, iconPath);
199
- // splash는 원본 사용
200
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
213
+ // logo.png: 1024x1024 (Easy Mode)
214
+ // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
215
+ const logoPath = path.resolve(resourcesDirPath, "logo.png");
216
+ 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
+ await this._cleanupExistingIconsAsync(capacitorPath);
201
223
  try {
202
- await SdCliCapacitor._execAsync("npx", ["@capacitor/assets", "generate", "--android"], capacitorPath);
224
+ await SdCliCapacitor._execAsync("npx", [
225
+ "@capacitor/assets",
226
+ "generate",
227
+ "--android",
228
+ "--iconBackgroundColor",
229
+ "#ffffff",
230
+ "--splashBackgroundColor",
231
+ "#ffffff",
232
+ ], capacitorPath);
203
233
  }
204
234
  catch {
205
- SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
235
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
206
236
  }
207
237
  }
208
238
  else {
209
- await FsUtils.removeAsync(iconDirPath);
239
+ await FsUtils.removeAsync(resourcesDirPath);
210
240
  }
211
241
  }
212
- async _createPaddedIconAsync(sourcePath, outputPath) {
213
- const outputSize = 1024;
214
- const iconSize = 680; // safe zone (66%)
215
- const padding = Math.floor((outputSize - iconSize) / 2); // 172px
216
- // 원본 크기 상관없이 iconSize로 리사이즈 후 여백 추가
242
+ // 중앙에 로고를 배치한 이미지 생성
243
+ async _createCenteredImageAsync(sourcePath, outputPath, outputSize, logoRatio) {
244
+ const logoSize = Math.floor(outputSize * logoRatio);
245
+ const padding = Math.floor((outputSize - logoSize) / 2);
217
246
  await sharp(sourcePath)
218
- .resize(iconSize, iconSize, {
247
+ .resize(logoSize, logoSize, {
219
248
  fit: "contain",
220
249
  background: { r: 0, g: 0, b: 0, alpha: 0 },
221
250
  })
@@ -228,6 +257,19 @@ export class SdCliCapacitor {
228
257
  })
229
258
  .toFile(outputPath);
230
259
  }
260
+ // 기존 아이콘 파일 삭제
261
+ async _cleanupExistingIconsAsync(capacitorPath) {
262
+ const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
263
+ if (!FsUtils.exists(androidResPath))
264
+ return;
265
+ const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
266
+ for (const dir of mipmapDirs) {
267
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
268
+ for (const file of iconFiles) {
269
+ await FsUtils.removeAsync(file);
270
+ }
271
+ }
272
+ }
231
273
  // 7. Android 네이티브 설정
232
274
  async _configureAndroidNativeAsync(capacitorPath) {
233
275
  const androidPath = path.resolve(capacitorPath, "android");
@@ -414,10 +456,12 @@ export class SdCliCapacitor {
414
456
  async buildAsync(outPath) {
415
457
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
416
458
  const buildType = this._opt.config.debug ? "debug" : "release";
417
- // 웹 자산 동기화
418
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
419
459
  // 플랫폼별 빌드
420
- await Promise.all(this._platforms.map((platform) => this._buildPlatformAsync(capacitorPath, outPath, platform, buildType)));
460
+ await Promise.all(this._platforms.map(async (platform) => {
461
+ // 해당 플랫폼만 copy
462
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
463
+ await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
464
+ }));
421
465
  }
422
466
  async _buildPlatformAsync(capacitorPath, outPath, platform, buildType) {
423
467
  if (platform === "android") {
@@ -509,7 +553,7 @@ export class SdCliCapacitor {
509
553
  }
510
554
  }
511
555
  // cap sync 후 run
512
- await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
556
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", opt.platform], capacitorPath);
513
557
  try {
514
558
  await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
515
559
  }
@@ -64,14 +64,16 @@ export class SdCliIndexFileGenerator {
64
64
  const indexFilePath = path.resolve(pkgPath, "src/index.ts");
65
65
  const tsconfig = await FsUtils.readJsonAsync(path.resolve(pkgPath, "tsconfig.json"));
66
66
  return [
67
- ...(tsconfig.excludes ?? []),
68
- ...(excludes ?? []),
69
67
  indexFilePath,
70
- path.resolve(pkgPath, "src/**/*.d.ts"),
71
- path.resolve(pkgPath, "src/index.ts"),
72
- path.resolve(pkgPath, "src/workers/**/*{.ts,.tsx}"),
73
- // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
74
- // path.resolve(pkgPath, "src/internal/**/*{.ts,.tsx}"),
68
+ ...[
69
+ ...(tsconfig.excludes ?? []),
70
+ ...(excludes ?? []),
71
+ "src/**/*.d.ts",
72
+ "src/index.ts",
73
+ "src/workers/**/*{.ts,.tsx}",
74
+ // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
75
+ // "src/internal/**/*{.ts,.tsx}",
76
+ ].map((item) => path.resolve(pkgPath, item)),
75
77
  ].map((item) => item.replace(/\\/g, "/"));
76
78
  }
77
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simplysm/sd-cli",
3
- "version": "12.16.8",
3
+ "version": "12.16.10",
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.8",
21
- "@simplysm/sd-core-node": "12.16.8",
22
- "@simplysm/sd-service-server": "12.16.8",
23
- "@simplysm/sd-storage": "12.16.8",
20
+ "@simplysm/sd-core-common": "12.16.10",
21
+ "@simplysm/sd-core-node": "12.16.10",
22
+ "@simplysm/sd-service-server": "12.16.10",
23
+ "@simplysm/sd-storage": "12.16.10",
24
24
  "browserslist": "^4.28.1",
25
25
  "cordova": "^13.0.0",
26
26
  "electron": "^33.4.11",
@@ -35,7 +35,7 @@ export class SdCliCapacitor {
35
35
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
36
36
 
37
37
  // 1. Capacitor 프로젝트 초기화
38
- await this._initializeCapacitorProjectAsync(capacitorPath);
38
+ const isNewProject = await this._initializeCapacitorProjectAsync(capacitorPath);
39
39
 
40
40
  // 2. Capacitor 설정 파일 생성
41
41
  await this._createCapacitorConfigAsync(capacitorPath);
@@ -44,7 +44,7 @@ export class SdCliCapacitor {
44
44
  await this._managePlatformsAsync(capacitorPath);
45
45
 
46
46
  // 4. 플러그인 관리
47
- await this._managePluginsAsync(capacitorPath);
47
+ const pluginsChanged = await this._managePluginsAsync(capacitorPath);
48
48
 
49
49
  // 5. 안드로이드 서명 설정
50
50
  await this._setupAndroidSignAsync(capacitorPath);
@@ -58,64 +58,70 @@ export class SdCliCapacitor {
58
58
  }
59
59
 
60
60
  // 8. 웹 자산 동기화
61
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
61
+ if (isNewProject || pluginsChanged) {
62
+ await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
63
+ } else {
64
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy"], capacitorPath);
65
+ }
62
66
  }
63
67
 
64
68
  // 1. Capacitor 프로젝트 초기화
65
- private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<void> {
69
+ private async _initializeCapacitorProjectAsync(capacitorPath: string): Promise<boolean> {
66
70
  if (FsUtils.exists(path.resolve(capacitorPath, "www"))) {
67
71
  SdCliCapacitor._logger.log("이미 생성되어있는 '.capacitor'를 사용합니다.");
68
72
 
69
73
  // 버전 동기화
70
74
  await this._syncVersionAsync(capacitorPath);
71
- } else {
72
- await FsUtils.mkdirsAsync(capacitorPath);
75
+ return false;
76
+ }
73
77
 
74
- // package.json 생성
75
- const projNpmConfig = await FsUtils.readJsonAsync(
76
- path.resolve(this._opt.pkgPath, "../../package.json"),
77
- );
78
- const pkgJson = {
79
- name: this._opt.config.appId,
80
- version: this._npmConfig.version,
81
- private: true,
82
- volta: projNpmConfig.volta,
83
- dependencies: {
84
- "@capacitor/core": "^7.0.0",
85
- "@capacitor/app": "^7.0.0",
86
- },
87
- devDependencies: {
88
- "@capacitor/cli": "^7.0.0",
89
- "@capacitor/assets": "^3.0.0",
90
- ...this._platforms.toObject(
91
- (item) => `@capacitor/${item}`,
92
- () => "^7.0.0",
93
- ),
94
- },
95
- };
96
- await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
97
- space: 2,
98
- });
99
-
100
- // .yarnrc.yml 작성
101
- await FsUtils.writeFileAsync(
102
- path.resolve(capacitorPath, ".yarnrc.yml"),
103
- "nodeLinker: node-modules",
104
- );
105
78
 
106
- // yarn.lock 작성
107
- await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
79
+ await FsUtils.mkdirsAsync(capacitorPath);
108
80
 
109
- // yarn install
110
- await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
81
+ // package.json 생성
82
+ const projNpmConfig = await FsUtils.readJsonAsync(
83
+ path.resolve(this._opt.pkgPath, "../../package.json"),
84
+ );
85
+ const pkgJson = {
86
+ name: this._opt.config.appId,
87
+ version: this._npmConfig.version,
88
+ private: true,
89
+ volta: projNpmConfig.volta,
90
+ dependencies: {
91
+ "@capacitor/core": "^7.0.0",
92
+ "@capacitor/app": "^7.0.0",
93
+ },
94
+ devDependencies: {
95
+ "@capacitor/cli": "^7.0.0",
96
+ "@capacitor/assets": "^3.0.0",
97
+ ...this._platforms.toObject(
98
+ (item) => `@capacitor/${item}`,
99
+ () => "^7.0.0",
100
+ ),
101
+ },
102
+ };
103
+ await FsUtils.writeJsonAsync(path.resolve(capacitorPath, "package.json"), pkgJson, {
104
+ space: 2,
105
+ });
111
106
 
112
- // capacitor init
113
- await SdCliCapacitor._execAsync(
114
- "npx",
115
- ["cap", "init", this._opt.config.appName, this._opt.config.appId],
116
- capacitorPath,
117
- );
118
- }
107
+ // .yarnrc.yml 작성
108
+ await FsUtils.writeFileAsync(
109
+ path.resolve(capacitorPath, ".yarnrc.yml"),
110
+ "nodeLinker: node-modules",
111
+ );
112
+
113
+ // yarn.lock 작성
114
+ await FsUtils.writeFileAsync(path.resolve(capacitorPath, "yarn.lock"), "");
115
+
116
+ // yarn install
117
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
118
+
119
+ // capacitor init
120
+ await SdCliCapacitor._execAsync(
121
+ "npx",
122
+ ["cap", "init", this._opt.config.appName, this._opt.config.appId],
123
+ capacitorPath,
124
+ );
119
125
 
120
126
  // www/index.html 생성
121
127
  const wwwPath = path.resolve(capacitorPath, "www");
@@ -123,6 +129,8 @@ export class SdCliCapacitor {
123
129
  path.resolve(wwwPath, "index.html"),
124
130
  "<!DOCTYPE html><html><head></head><body></body></html>",
125
131
  );
132
+
133
+ return true;
126
134
  }
127
135
 
128
136
  // 버전 동기화
@@ -192,51 +200,62 @@ export class SdCliCapacitor {
192
200
  }
193
201
  }
194
202
 
195
- // 4. 플러그인 관리
196
- private async _managePluginsAsync(capacitorPath: string): Promise<void> {
203
+ private async _managePluginsAsync(capacitorPath: string): Promise<boolean> {
197
204
  const pkgJsonPath = path.resolve(capacitorPath, "package.json");
198
205
  const pkgJson = (await FsUtils.readJsonAsync(pkgJsonPath)) as INpmConfig;
199
- const currentDeps = Object.keys(pkgJson.dependencies ?? {});
206
+
207
+ const mainDeps = {
208
+ ...this._npmConfig.dependencies,
209
+ ...this._npmConfig.devDependencies,
210
+ ...this._npmConfig.peerDependencies,
211
+ };
200
212
 
201
213
  const usePlugins = Object.keys(this._opt.config.plugins ?? {});
214
+ const currentDeps = pkgJson.dependencies ?? {};
215
+
216
+ let changed = false;
202
217
 
203
218
  // 사용하지 않는 플러그인 제거
204
- for (const dep of currentDeps) {
205
- // @capacitor/core, @capacitor/android 기본 패키지는 제외
206
- if (
207
- dep.startsWith("@capacitor/") &&
208
- ["core", "android", "ios"].some((p) => dep.endsWith(p))
209
- ) {
210
- continue;
219
+ for (const dep of Object.keys(currentDeps)) {
220
+ if (this._isCapacitorPlugin(dep) && !usePlugins.includes(dep)) {
221
+ delete currentDeps[dep];
222
+ changed = true;
223
+ SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
211
224
  }
225
+ }
212
226
 
213
- // 플러그인 목록에 없는 패키지는 제거
214
- if (!usePlugins.includes(dep)) {
215
- // Capacitor 관련 플러그인만 제거
216
- if (dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin")) {
217
- await SdCliCapacitor._execAsync("yarn", ["remove", dep], capacitorPath);
218
- SdCliCapacitor._logger.debug(`플러그인 제거: ${dep}`);
219
- }
227
+ // 플러그인 추가
228
+ for (const plugin of usePlugins) {
229
+ if (!(plugin in currentDeps)) {
230
+ const version = mainDeps[plugin] ?? "^7.0.0";
231
+ currentDeps[plugin] = version;
232
+ changed = true;
233
+ SdCliCapacitor._logger.debug(`플러그인 추가: ${plugin}@${version}`);
220
234
  }
221
235
  }
222
236
 
223
- // 플러그인 설치
224
- const mainDeps = {
225
- ...this._npmConfig.dependencies,
226
- ...this._npmConfig.devDependencies,
227
- ...this._npmConfig.peerDependencies,
228
- };
237
+ // 변경사항 있을 때만 저장 & install
238
+ if (changed) {
239
+ pkgJson.dependencies = currentDeps;
240
+ await FsUtils.writeJsonAsync(pkgJsonPath, pkgJson, { space: 2 });
241
+ await SdCliCapacitor._execAsync("yarn", ["install"], capacitorPath);
242
+ return true;
243
+ }
244
+ // 변경 없으면 아무것도 안 함 → 오프라인 OK
245
+ return false;
246
+ }
229
247
 
230
- for (const plugin of usePlugins) {
231
- if (!currentDeps.includes(plugin)) {
232
- // 메인 프로젝트에 버전이 있으면 그 버전으로 설치
233
- const version = mainDeps[plugin];
234
- const pluginWithVersion = version ? `${plugin}@${version}` : plugin;
248
+ private _isCapacitorPlugin(dep: string): boolean {
249
+ // 기본 패키지 제외
250
+ const corePackages = [
251
+ "@capacitor/core",
252
+ "@capacitor/android",
253
+ "@capacitor/ios",
254
+ "@capacitor/app",
255
+ ];
256
+ if (corePackages.includes(dep)) return false;
235
257
 
236
- await SdCliCapacitor._execAsync("yarn", ["add", pluginWithVersion], capacitorPath);
237
- SdCliCapacitor._logger.debug(`플러그인 설치: ${pluginWithVersion}`);
238
- }
239
- }
258
+ return dep.startsWith("@capacitor/") || dep.includes("capacitor-plugin");
240
259
  }
241
260
 
242
261
  // 5. 안드로이드 서명 설정
@@ -255,42 +274,60 @@ export class SdCliCapacitor {
255
274
 
256
275
  // 6. 아이콘 및 스플래시 스크린 설정
257
276
  private async _setupIconAndSplashScreenAsync(capacitorPath: string): Promise<void> {
258
- const iconDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
277
+ const resourcesDirPath = path.resolve(capacitorPath, this._ICON_DIR_PATH);
259
278
 
260
279
  if (this._opt.config.icon != null) {
261
- await FsUtils.mkdirsAsync(iconDirPath);
280
+ await FsUtils.mkdirsAsync(resourcesDirPath);
262
281
 
263
282
  const iconSource = path.resolve(this._opt.pkgPath, this._opt.config.icon);
264
283
 
265
- // icon.png 자체를 여백 포함으로 생성
266
- const iconPath = path.resolve(iconDirPath, "icon.png");
267
- await this._createPaddedIconAsync(iconSource, iconPath);
284
+ // logo.png: 1024x1024 (Easy Mode)
285
+ // 로고 크기 약 60% - safe zone(61%) 내에서 최대한 크게
286
+ const logoPath = path.resolve(resourcesDirPath, "logo.png");
287
+ await this._createCenteredImageAsync(iconSource, logoPath, 1024, 0.6);
268
288
 
269
- // splash 원본 사용
270
- await FsUtils.copyAsync(iconSource, path.resolve(iconDirPath, "splash.png"));
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
+ await this._cleanupExistingIconsAsync(capacitorPath);
271
296
 
272
297
  try {
273
298
  await SdCliCapacitor._execAsync(
274
299
  "npx",
275
- ["@capacitor/assets", "generate", "--android"],
300
+ [
301
+ "@capacitor/assets",
302
+ "generate",
303
+ "--android",
304
+ "--iconBackgroundColor",
305
+ "#ffffff",
306
+ "--splashBackgroundColor",
307
+ "#ffffff",
308
+ ],
276
309
  capacitorPath,
277
310
  );
278
311
  } catch {
279
- SdCliCapacitor._logger.warn("아이콘 리사이징 실패, 기본 아이콘 사용");
312
+ SdCliCapacitor._logger.warn("아이콘 생성 실패, 기본 아이콘 사용");
280
313
  }
281
314
  } else {
282
- await FsUtils.removeAsync(iconDirPath);
315
+ await FsUtils.removeAsync(resourcesDirPath);
283
316
  }
284
317
  }
285
318
 
286
- private async _createPaddedIconAsync(sourcePath: string, outputPath: string): Promise<void> {
287
- const outputSize = 1024;
288
- const iconSize = 680; // safe zone (66%)
289
- const padding = Math.floor((outputSize - iconSize) / 2); // 172px
319
+ // 중앙에 로고를 배치한 이미지 생성
320
+ private async _createCenteredImageAsync(
321
+ sourcePath: string,
322
+ outputPath: string,
323
+ outputSize: number,
324
+ logoRatio: number, // 0.0 ~ 1.0
325
+ ): Promise<void> {
326
+ const logoSize = Math.floor(outputSize * logoRatio);
327
+ const padding = Math.floor((outputSize - logoSize) / 2);
290
328
 
291
- // 원본 크기 상관없이 iconSize로 리사이즈 후 여백 추가
292
329
  await sharp(sourcePath)
293
- .resize(iconSize, iconSize, {
330
+ .resize(logoSize, logoSize, {
294
331
  fit: "contain",
295
332
  background: { r: 0, g: 0, b: 0, alpha: 0 },
296
333
  })
@@ -304,6 +341,21 @@ export class SdCliCapacitor {
304
341
  .toFile(outputPath);
305
342
  }
306
343
 
344
+ // 기존 아이콘 파일 삭제
345
+ private async _cleanupExistingIconsAsync(capacitorPath: string): Promise<void> {
346
+ const androidResPath = path.resolve(capacitorPath, "android/app/src/main/res");
347
+
348
+ if (!FsUtils.exists(androidResPath)) return;
349
+
350
+ const mipmapDirs = await FsUtils.globAsync(path.resolve(androidResPath, "mipmap-*"));
351
+ for (const dir of mipmapDirs) {
352
+ const iconFiles = await FsUtils.globAsync(path.resolve(dir, "ic_launcher*.png"));
353
+ for (const file of iconFiles) {
354
+ await FsUtils.removeAsync(file);
355
+ }
356
+ }
357
+ }
358
+
307
359
  // 7. Android 네이티브 설정
308
360
  private async _configureAndroidNativeAsync(capacitorPath: string) {
309
361
  const androidPath = path.resolve(capacitorPath, "android");
@@ -572,14 +624,13 @@ export class SdCliCapacitor {
572
624
  const capacitorPath = path.resolve(this._opt.pkgPath, this._CAPACITOR_DIR_NAME);
573
625
  const buildType = this._opt.config.debug ? "debug" : "release";
574
626
 
575
- // 웹 자산 동기화
576
- await SdCliCapacitor._execAsync("npx", ["cap", "sync"], capacitorPath);
577
-
578
627
  // 플랫폼별 빌드
579
628
  await Promise.all(
580
- this._platforms.map((platform) =>
581
- this._buildPlatformAsync(capacitorPath, outPath, platform, buildType),
582
- ),
629
+ this._platforms.map(async (platform) => {
630
+ // 해당 플랫폼만 copy
631
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", platform], capacitorPath);
632
+ await this._buildPlatformAsync(capacitorPath, outPath, platform, buildType);
633
+ }),
583
634
  );
584
635
  }
585
636
 
@@ -736,7 +787,7 @@ export class SdCliCapacitor {
736
787
  }
737
788
 
738
789
  // cap sync 후 run
739
- await this._execAsync("npx", ["cap", "sync", opt.platform], capacitorPath);
790
+ await SdCliCapacitor._execAsync("npx", ["cap", "copy", opt.platform], capacitorPath);
740
791
 
741
792
  try {
742
793
  await this._execAsync("npx", ["cap", "run", opt.platform], capacitorPath);
@@ -78,15 +78,17 @@ export class SdCliIndexFileGenerator {
78
78
  const tsconfig = await FsUtils.readJsonAsync(path.resolve(pkgPath, "tsconfig.json"));
79
79
 
80
80
  return [
81
- ...(tsconfig.excludes ?? []),
82
- ...(excludes ?? []),
83
81
  indexFilePath,
84
- path.resolve(pkgPath, "src/**/*.d.ts"),
85
- path.resolve(pkgPath, "src/index.ts"),
86
- path.resolve(pkgPath, "src/workers/**/*{.ts,.tsx}"),
87
-
88
- // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
89
- // path.resolve(pkgPath, "src/internal/**/*{.ts,.tsx}"),
82
+ ...[
83
+ ...(tsconfig.excludes ?? []),
84
+ ...(excludes ?? []),
85
+ "src/**/*.d.ts",
86
+ "src/index.ts",
87
+ "src/workers/**/*{.ts,.tsx}",
88
+
89
+ // TODO: index에 없는 파일은 watch가 안됨... 처리 필요함.
90
+ // "src/internal/**/*{.ts,.tsx}",
91
+ ].map((item) => path.resolve(pkgPath, item)),
90
92
  ].map((item) => item.replace(/\\/g, "/"));
91
93
  }
92
94
  }