create-ait-app 0.0.2 → 0.0.3

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 (112) hide show
  1. package/README.md +38 -38
  2. package/package.json +5 -8
  3. package/src/cli.js +52 -0
  4. package/src/main.js +95 -444
  5. package/src/sample-configs.js +140 -0
  6. package/src/sample-inject.js +65 -0
  7. package/src/scaffold.js +111 -0
  8. package/src/skills.js +66 -0
  9. package/src/templates.js +88 -0
  10. package/src/utils/copy-dir.js +19 -0
  11. package/src/utils/fetch-text.js +47 -0
  12. package/src/utils/package-name.js +26 -0
  13. package/templates/js/eslint.config.js +14 -0
  14. package/templates/js/index.html +12 -0
  15. package/templates/js/package.json +20 -0
  16. package/templates/js/samples/iaa/src/lib/inAppAds.js +119 -0
  17. package/templates/js/samples/iaa/src/pages/InAppAdsPage.js +83 -0
  18. package/templates/js/samples/iap/src/lib/inAppPurchase.js +105 -0
  19. package/templates/js/samples/iap/src/pages/InAppPurchasePage.js +102 -0
  20. package/templates/js/src/app.js +58 -0
  21. package/templates/js/src/main.js +2 -0
  22. package/templates/js/vite.config.js +3 -0
  23. package/templates/react/README.md +26 -0
  24. package/templates/react/eslint.config.js +30 -0
  25. package/templates/react/granite.config.ts +20 -0
  26. package/templates/react/index.html +12 -0
  27. package/templates/react/package.json +27 -0
  28. package/templates/react/public/appsintoss-logo.png +0 -0
  29. package/templates/react/samples/iaa/src/hooks/useInAppAds.js +102 -0
  30. package/templates/react/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  31. package/templates/react/samples/iaa/src/pages/InAppAdsPage.jsx +75 -0
  32. package/templates/react/samples/iap/public/icon-document.png +0 -0
  33. package/templates/react/samples/iap/src/hooks/useInAppPurchase.js +95 -0
  34. package/templates/react/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  35. package/templates/react/samples/iap/src/pages/InAppPurchasePage.jsx +115 -0
  36. package/templates/react/src/App.css +104 -0
  37. package/templates/react/src/App.jsx +45 -0
  38. package/templates/react/src/index.css +27 -0
  39. package/templates/react/src/main.jsx +10 -0
  40. package/templates/react/vite.config.js +6 -0
  41. package/templates/react-ts/README.md +26 -0
  42. package/templates/react-ts/eslint.config.js +28 -0
  43. package/templates/react-ts/granite.config.ts +20 -0
  44. package/templates/react-ts/index.html +12 -0
  45. package/templates/react-ts/public/appsintoss-logo.png +0 -0
  46. package/templates/react-ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  47. package/templates/react-ts/samples/iap/public/icon-document.png +0 -0
  48. package/templates/react-ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  49. package/templates/react-ts/src/App.css +104 -0
  50. package/templates/react-ts/src/index.css +27 -0
  51. package/templates/react-ts/src/vite-env.d.ts +6 -0
  52. package/templates/react-ts/tsconfig.app.json +22 -0
  53. package/templates/react-ts/tsconfig.json +7 -0
  54. package/templates/react-ts/tsconfig.node.json +20 -0
  55. package/templates/react-ts/vite.config.ts +6 -0
  56. package/templates/react-ts-tds/README.md +26 -0
  57. package/templates/react-ts-tds/granite.config.ts +20 -0
  58. package/templates/react-ts-tds/package.json +35 -0
  59. package/templates/react-ts-tds/public/appsintoss-logo.png +0 -0
  60. package/templates/ts/README.md +26 -0
  61. package/templates/ts/eslint.config.js +15 -0
  62. package/templates/ts/granite.config.ts +20 -0
  63. package/templates/ts/index.html +12 -0
  64. package/templates/ts/package.json +22 -0
  65. package/templates/ts/public/appsintoss-logo.png +0 -0
  66. package/templates/ts/samples/iaa/src/lib/inAppAds.ts +132 -0
  67. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.css +72 -0
  68. package/templates/ts/samples/iaa/src/pages/InAppAdsPage.ts +85 -0
  69. package/templates/ts/samples/iap/public/icon-document.png +0 -0
  70. package/templates/ts/samples/iap/src/lib/inAppPurchase.ts +114 -0
  71. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.css +115 -0
  72. package/templates/ts/samples/iap/src/pages/InAppPurchasePage.ts +105 -0
  73. package/templates/ts/src/App.css +104 -0
  74. package/templates/ts/src/app.ts +60 -0
  75. package/templates/ts/src/index.css +27 -0
  76. package/templates/ts/src/main.ts +2 -0
  77. package/templates/ts/src/vite-env.d.ts +1 -0
  78. package/templates/ts/tsconfig.app.json +22 -0
  79. package/templates/ts/tsconfig.json +7 -0
  80. package/templates/ts/tsconfig.node.json +20 -0
  81. package/templates/ts/vite.config.ts +3 -0
  82. /package/{template → templates/js}/README.md +0 -0
  83. /package/{template → templates/js}/granite.config.ts +0 -0
  84. /package/{template → templates/js}/public/appsintoss-logo.png +0 -0
  85. /package/{template/__default/__samples → templates/js/samples}/iaa/src/pages/InAppAdsPage.css +0 -0
  86. /package/{template/__default/__samples → templates/js/samples}/iap/public/icon-document.png +0 -0
  87. /package/{template/__default/__samples → templates/js/samples}/iap/src/pages/InAppPurchasePage.css +0 -0
  88. /package/{template/__default → templates/js}/src/App.css +0 -0
  89. /package/{template/__default → templates/js}/src/index.css +0 -0
  90. /package/{template → templates/react-ts}/package.json +0 -0
  91. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  92. /package/{template/__default/__samples → templates/react-ts/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  93. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  94. /package/{template/__default/__samples → templates/react-ts/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  95. /package/{template/__default → templates/react-ts}/src/App.tsx +0 -0
  96. /package/{template/__default → templates/react-ts}/src/main.tsx +0 -0
  97. /package/{template → templates/react-ts-tds}/eslint.config.js +0 -0
  98. /package/{template → templates/react-ts-tds}/index.html +0 -0
  99. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/hooks/useInAppAds.tsx +0 -0
  100. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iaa/src/pages/InAppAdsPage.tsx +0 -0
  101. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/public/icon-document.png +0 -0
  102. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/hooks/useInAppPurchase.ts +0 -0
  103. /package/{template/__tds/__samples → templates/react-ts-tds/samples}/iap/src/pages/InAppPurchasePage.tsx +0 -0
  104. /package/{template/__tds → templates/react-ts-tds}/src/App.css +0 -0
  105. /package/{template/__tds → templates/react-ts-tds}/src/App.tsx +0 -0
  106. /package/{template/__tds → templates/react-ts-tds}/src/index.css +0 -0
  107. /package/{template/__tds → templates/react-ts-tds}/src/main.tsx +0 -0
  108. /package/{template → templates/react-ts-tds}/src/vite-env.d.ts +0 -0
  109. /package/{template → templates/react-ts-tds}/tsconfig.app.json +0 -0
  110. /package/{template → templates/react-ts-tds}/tsconfig.json +0 -0
  111. /package/{template → templates/react-ts-tds}/tsconfig.node.json +0 -0
  112. /package/{template → templates/react-ts-tds}/vite.config.ts +0 -0
@@ -0,0 +1,119 @@
1
+ import {
2
+ loadFullScreenAd,
3
+ showFullScreenAd,
4
+ } from "@apps-in-toss/web-framework";
5
+
6
+ // 참고문서: https://developers-apps-in-toss.toss.im/ads/intro.html
7
+ export function createInAppAds(adGroupId) {
8
+ const state = {
9
+ isAdLoaded: false,
10
+ isSupported: false,
11
+ lastReward: null,
12
+ };
13
+
14
+ let onUpdate = () => {};
15
+ let unregister = null;
16
+
17
+ function notify() {
18
+ onUpdate();
19
+ }
20
+
21
+ function load() {
22
+ state.isAdLoaded = false;
23
+ notify();
24
+
25
+ try {
26
+ unregister = loadFullScreenAd({
27
+ options: { adGroupId },
28
+ onEvent: (event) => {
29
+ if (event.type === "loaded") {
30
+ state.isAdLoaded = true;
31
+ notify();
32
+ }
33
+ },
34
+ onError: (error) => {
35
+ console.error("광고 로드 실패:", error);
36
+ },
37
+ });
38
+ } catch (error) {
39
+ alert(
40
+ "광고 로드 실패: \n\n- 인앱광고 기능은 브라우저가 아닌 샌드박스앱/토스앱에서 실행해주세요\n\n" +
41
+ error,
42
+ );
43
+ }
44
+ }
45
+
46
+ function showAd() {
47
+ if (!state.isSupported) {
48
+ console.info("현재 환경에서는 인앱 광고가 지원되지 않습니다.");
49
+ return;
50
+ }
51
+
52
+ if (!state.isAdLoaded) {
53
+ console.info("아직 광고가 로드되지 않았습니다.");
54
+ return;
55
+ }
56
+
57
+ try {
58
+ showFullScreenAd({
59
+ options: { adGroupId },
60
+ onEvent: (event) => {
61
+ switch (event.type) {
62
+ case "userEarnedReward":
63
+ state.lastReward = event.data;
64
+ notify();
65
+ break;
66
+ case "dismissed":
67
+ state.isAdLoaded = false;
68
+ notify();
69
+ load();
70
+ break;
71
+ case "failedToShow":
72
+ console.error("광고 표시 실패");
73
+ state.isAdLoaded = false;
74
+ notify();
75
+ load();
76
+ break;
77
+ }
78
+ },
79
+ onError: (error) => {
80
+ console.error("광고 표시 실패:", error);
81
+ state.isAdLoaded = false;
82
+ notify();
83
+ load();
84
+ },
85
+ });
86
+ } catch (error) {
87
+ console.error("광고 표시 실패:", error);
88
+ state.isAdLoaded = false;
89
+ notify();
90
+ load();
91
+ }
92
+ }
93
+
94
+ try {
95
+ state.isSupported = loadFullScreenAd.isSupported();
96
+ if (state.isSupported) {
97
+ load();
98
+ }
99
+ } catch (error) {
100
+ console.error("광고 지원 여부 확인 실패:", error);
101
+ state.isSupported = false;
102
+ }
103
+
104
+ return {
105
+ getState: () => state,
106
+ showAd,
107
+ subscribe: (listener) => {
108
+ onUpdate = listener;
109
+ return () => {
110
+ onUpdate = () => {};
111
+ try {
112
+ unregister?.();
113
+ } catch (error) {
114
+ console.error("광고 정리(cleanup) 중 에러:", error);
115
+ }
116
+ };
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,83 @@
1
+ import { createInAppAds } from "../lib/inAppAds.js";
2
+ import "./InAppAdsPage.css";
3
+
4
+ // TODO: 서비스를 출시하기 전에 앱인토스 콘솔에서 발급한 광고그룹ID로 변경해주세요.
5
+ const TEST_INTERSTITIAL_ID = "ait-ad-test-interstitial-id";
6
+ const TEST_REWARDED_ID = "ait-ad-test-rewarded-id";
7
+
8
+ export function mountInAppAdsPage(onBack) {
9
+ const root = document.getElementById("root");
10
+ const interstitial = createInAppAds(TEST_INTERSTITIAL_ID);
11
+ const rewarded = createInAppAds(TEST_REWARDED_ID);
12
+
13
+ function render() {
14
+ const interstitialState = interstitial.getState();
15
+ const rewardedState = rewarded.getState();
16
+
17
+ root.innerHTML = `
18
+ <div class="app-header">
19
+ <h1 class="page-title">인앱광고</h1>
20
+ ${
21
+ !interstitialState.isSupported
22
+ ? '<p class="page-subtitle">이 환경에서는 인앱 광고를 사용할 수 없어요.</p>'
23
+ : ""
24
+ }
25
+ </div>
26
+
27
+ <div class="iaa-section-list">
28
+ <div class="iaa-section">
29
+ <div class="iaa-section-row">
30
+ <div class="iaa-section-info">
31
+ <h2 class="iaa-section-title">전면형 광고</h2>
32
+ <p class="iaa-section-desc">화면 전체에 표시되는 광고</p>
33
+ </div>
34
+ <button
35
+ type="button"
36
+ class="iaa-section-button"
37
+ data-action="show-interstitial"
38
+ ${interstitialState.isAdLoaded ? "" : "disabled"}
39
+ >
40
+ ${interstitialState.isAdLoaded ? "보기" : "로딩 중"}
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ <div class="iaa-section">
46
+ <div class="iaa-section-row">
47
+ <div class="iaa-section-info">
48
+ <h2 class="iaa-section-title">보상형 광고</h2>
49
+ <p class="iaa-section-desc">시청 완료 시 보상을 받는 광고</p>
50
+ </div>
51
+ <button
52
+ type="button"
53
+ class="iaa-section-button"
54
+ data-action="show-rewarded"
55
+ ${rewardedState.isAdLoaded ? "" : "disabled"}
56
+ >
57
+ ${rewardedState.isAdLoaded ? "보기" : "로딩 중"}
58
+ </button>
59
+ </div>
60
+ ${
61
+ rewardedState.lastReward
62
+ ? `<p class="iaa-reward-message">보상 획득: ${rewardedState.lastReward.unitType} ${rewardedState.lastReward.unitAmount}개</p>`
63
+ : ""
64
+ }
65
+ </div>
66
+ </div>
67
+
68
+ <button type="button" class="text-button iaa-back-btn" data-action="back">← 홈으로</button>
69
+ `;
70
+
71
+ root
72
+ .querySelector('[data-action="show-interstitial"]')
73
+ ?.addEventListener("click", () => interstitial.showAd());
74
+ root
75
+ .querySelector('[data-action="show-rewarded"]')
76
+ ?.addEventListener("click", () => rewarded.showAd());
77
+ root.querySelector('[data-action="back"]')?.addEventListener("click", onBack);
78
+ }
79
+
80
+ interstitial.subscribe(render);
81
+ rewarded.subscribe(render);
82
+ render();
83
+ }
@@ -0,0 +1,105 @@
1
+ import { IAP } from "@apps-in-toss/web-framework";
2
+
3
+ // 참고문서: https://developers-apps-in-toss.toss.im/iap/intro.html
4
+ export function createInAppPurchase() {
5
+ const state = {
6
+ products: [],
7
+ productsLoading: false,
8
+ purchasingSku: null,
9
+ };
10
+
11
+ let onUpdate = () => {};
12
+
13
+ function notify() {
14
+ onUpdate();
15
+ }
16
+
17
+ function grantProduct(orderId) {
18
+ // TODO: 여기에 상품 지급 로직을 작성해주세요.
19
+ console.info(`상품 지급 처리: ${orderId}`);
20
+ }
21
+
22
+ async function fetchProducts() {
23
+ state.productsLoading = true;
24
+ notify();
25
+
26
+ try {
27
+ const response = await IAP.getProductItemList();
28
+ state.products = response?.products ?? [];
29
+ } catch (error) {
30
+ alert(
31
+ "상품 목록 조회 실패: \n\n-앱인토스 콘솔에서 미니앱을 생성후 인앱상품을 등록해주세요\n- 인앱결제 기능은 브라우저가 아닌 샌드박스앱/토스앱에서 실행해주세요\n\n" +
32
+ error,
33
+ );
34
+ } finally {
35
+ state.productsLoading = false;
36
+ notify();
37
+ }
38
+ }
39
+
40
+ function purchaseProduct(sku) {
41
+ state.purchasingSku = sku;
42
+ notify();
43
+
44
+ try {
45
+ const cleanup = IAP.createOneTimePurchaseOrder({
46
+ options: {
47
+ sku,
48
+ processProductGrant: ({ orderId }) => {
49
+ grantProduct(orderId);
50
+ console.info(`상품 지급 처리: ${orderId}`);
51
+ return true;
52
+ },
53
+ },
54
+ onEvent: (event) => {
55
+ if (event.type === "success") {
56
+ alert(`${event.data.displayName}이 결제되었어요.`);
57
+ }
58
+
59
+ state.purchasingSku = null;
60
+ notify();
61
+ cleanup();
62
+ },
63
+ onError: (error) => {
64
+ console.error("인앱결제 실패:", error);
65
+ state.purchasingSku = null;
66
+ notify();
67
+ cleanup();
68
+ },
69
+ });
70
+ } catch (error) {
71
+ console.error("인앱결제 실패:", error);
72
+ state.purchasingSku = null;
73
+ notify();
74
+ }
75
+ }
76
+
77
+ async function restorePendingOrders() {
78
+ try {
79
+ const pending = await IAP.getPendingOrders();
80
+ const orders = pending?.orders ?? [];
81
+
82
+ for (const order of orders) {
83
+ grantProduct(order.orderId);
84
+ await IAP.completeProductGrant({ params: { orderId: order.orderId } });
85
+ console.info("미결 주문 복원 완료:", order.orderId);
86
+ }
87
+ } catch (error) {
88
+ console.error("주문 복원 실패:", error);
89
+ }
90
+ }
91
+
92
+ fetchProducts();
93
+
94
+ return {
95
+ getState: () => state,
96
+ purchaseProduct,
97
+ restorePendingOrders,
98
+ subscribe: (listener) => {
99
+ onUpdate = listener;
100
+ return () => {
101
+ onUpdate = () => {};
102
+ };
103
+ },
104
+ };
105
+ }
@@ -0,0 +1,102 @@
1
+ import { createInAppPurchase } from "../lib/inAppPurchase.js";
2
+ import "./InAppPurchasePage.css";
3
+
4
+ function escapeHtml(value) {
5
+ return String(value)
6
+ .replaceAll("&", "&amp;")
7
+ .replaceAll("<", "&lt;")
8
+ .replaceAll(">", "&gt;")
9
+ .replaceAll('"', "&quot;")
10
+ .replaceAll("'", "&#39;");
11
+ }
12
+
13
+ export function mountInAppPurchasePage(onBack) {
14
+ const root = document.getElementById("root");
15
+ const iap = createInAppPurchase();
16
+
17
+ function render() {
18
+ const { products, productsLoading, purchasingSku } = iap.getState();
19
+
20
+ if (productsLoading) {
21
+ root.innerHTML = `
22
+ <div class="app-header">
23
+ <h1 class="page-title">인앱결제</h1>
24
+ </div>
25
+ <p>상품을 불러오는 중...</p>
26
+ `;
27
+ return;
28
+ }
29
+
30
+ if (products.length === 0) {
31
+ root.innerHTML = `
32
+ <div class="app-header">
33
+ <h1 class="page-title">인앱결제</h1>
34
+ </div>
35
+ <div class="iap-empty-state">
36
+ <img class="iap-empty-state-icon" src="icon-document.png" aria-hidden alt="" />
37
+ <div class="iap-empty-state-content">
38
+ <h2 class="iap-empty-state-title">인앱 상품이 없어요</h2>
39
+ <p class="iap-empty-state-desc">콘솔 '인앱 결제' 메뉴에서 상품을 등록해 주세요.</p>
40
+ </div>
41
+ <button type="button" class="iap-empty-state-back-btn" data-action="back">홈으로</button>
42
+ </div>
43
+ `;
44
+ root.querySelector('[data-action="back"]')?.addEventListener("click", onBack);
45
+ return;
46
+ }
47
+
48
+ const productItems = products
49
+ .map(
50
+ (product) => `
51
+ <div class="iap-product-item">
52
+ <div class="iap-product-info">
53
+ ${
54
+ product.iconUrl
55
+ ? `<img class="iap-product-icon" src="${escapeHtml(product.iconUrl)}" alt="${escapeHtml(product.displayName)}" />`
56
+ : ""
57
+ }
58
+ <div class="iap-product-details">
59
+ <div class="iap-product-name">${escapeHtml(product.displayName)}</div>
60
+ ${
61
+ product.description
62
+ ? `<div class="iap-product-description">${escapeHtml(product.description)}</div>`
63
+ : ""
64
+ }
65
+ <div class="iap-product-amount">${escapeHtml(product.displayAmount)}</div>
66
+ </div>
67
+ </div>
68
+ <button
69
+ type="button"
70
+ class="iap-product-buy-btn"
71
+ data-sku="${escapeHtml(product.sku)}"
72
+ ${purchasingSku !== null ? "disabled" : ""}
73
+ >
74
+ ${purchasingSku === product.sku ? "결제 처리 중" : "구매하기"}
75
+ </button>
76
+ </div>
77
+ `,
78
+ )
79
+ .join("");
80
+
81
+ root.innerHTML = `
82
+ <div class="app-header">
83
+ <h1 class="page-title">인앱결제</h1>
84
+ </div>
85
+ <div class="iap-product-list">
86
+ ${productItems}
87
+ <button type="button" class="text-button iap-back-btn" data-action="back">← 홈으로</button>
88
+ </div>
89
+ `;
90
+
91
+ root.querySelector('[data-action="back"]')?.addEventListener("click", onBack);
92
+ root.querySelectorAll("[data-sku]").forEach((button) => {
93
+ button.addEventListener("click", () => {
94
+ iap.purchaseProduct(button.dataset.sku);
95
+ });
96
+ });
97
+ }
98
+
99
+ iap.subscribe(render);
100
+ iap.restorePendingOrders();
101
+ render();
102
+ }
@@ -0,0 +1,58 @@
1
+ import "./App.css";
2
+ {{SAMPLE_IMPORTS}}
3
+
4
+ let currentPage = null;
5
+
6
+ function renderHome() {
7
+ const root = document.getElementById("root");
8
+ root.innerHTML = `
9
+ <div class="app">
10
+ <header class="app-header">
11
+ <h1 class="page-title">반가워요</h1>
12
+ <p class="page-subtitle">앱인토스 개발을 시작해 보세요.</p>
13
+ </header>
14
+
15
+ <div class="app-actions">
16
+ <a
17
+ class="app-button app-button-primary"
18
+ href="https://developers-apps-in-toss.toss.im"
19
+ target="_blank"
20
+ rel="noopener noreferrer"
21
+ >
22
+ 개발자센터
23
+ </a>
24
+ <a
25
+ class="app-button app-button-primary"
26
+ href="https://techchat-apps-in-toss.toss.im"
27
+ target="_blank"
28
+ rel="noopener noreferrer"
29
+ >
30
+ 개발자 커뮤니티
31
+ </a>
32
+ {{SAMPLE_BUTTONS}}
33
+ </div>
34
+
35
+ <div class="app-logo-wrap">
36
+ <img
37
+ class="logo"
38
+ src="${import.meta.env.BASE_URL}appsintoss-logo.png"
39
+ alt="apps in toss"
40
+ />
41
+ </div>
42
+ </div>
43
+ `;
44
+
45
+ root.querySelectorAll("[data-page]").forEach((button) => {
46
+ button.addEventListener("click", () => {
47
+ currentPage = button.dataset.page;
48
+ render();
49
+ });
50
+ });
51
+ }
52
+
53
+ function render() {
54
+ {{SAMPLE_ROUTES}}
55
+ renderHome();
56
+ }
57
+
58
+ render();
@@ -0,0 +1,2 @@
1
+ import "./index.css";
2
+ import "./app.js";
@@ -0,0 +1,3 @@
1
+ import { defineConfig } from "vite";
2
+
3
+ export default defineConfig({});
@@ -0,0 +1,26 @@
1
+ # {{APP_NAME}}
2
+
3
+ Apps in Toss 프로젝트입니다.
4
+
5
+ ## 시작하기
6
+
7
+ ```bash
8
+ {{PM_DEV}}
9
+ ```
10
+
11
+ ## 배포하기
12
+
13
+ - 앱인토스 배포 API 키는 [앱인토스 콘솔](https://apps-in-toss.toss.im/) > 워크스페이스 > API 키 > 콘솔 API 키 에서 발급받을 수 있어요.
14
+
15
+ ```bash
16
+ {{PM_BUILD}}
17
+ {{PM_DEPLOY}}
18
+ ```
19
+
20
+ ## 유용한 링크
21
+
22
+ - [앱인토스 콘솔](https://apps-in-toss.toss.im/)
23
+ - [앱인토스 개발자센터](https://developers-apps-in-toss.toss.im/)
24
+ - [앱인토스 개발자 커뮤니티](https://techchat-apps-in-toss.toss.im/)
25
+
26
+ AI를 사용하시는 경우 [여기](https://developers-apps-in-toss.toss.im/development/llms.html)를 확인해보세요.
@@ -0,0 +1,30 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+ import reactHooks from "eslint-plugin-react-hooks";
4
+ import reactRefresh from "eslint-plugin-react-refresh";
5
+
6
+ export default [
7
+ { ignores: ["dist"] },
8
+ {
9
+ files: ["**/*.{js,jsx}"],
10
+ ...js.configs.recommended,
11
+ languageOptions: {
12
+ ecmaVersion: 2020,
13
+ globals: globals.browser,
14
+ parserOptions: {
15
+ ecmaFeatures: { jsx: true },
16
+ },
17
+ },
18
+ plugins: {
19
+ "react-hooks": reactHooks,
20
+ "react-refresh": reactRefresh,
21
+ },
22
+ rules: {
23
+ ...reactHooks.configs.recommended.rules,
24
+ "react-refresh/only-export-components": [
25
+ "warn",
26
+ { allowConstantExport: true },
27
+ ],
28
+ },
29
+ },
30
+ ];
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "@apps-in-toss/web-framework/config";
2
+
3
+ export default defineConfig({
4
+ appName: "{{APP_NAME}}",
5
+ brand: {
6
+ displayName: "앱 이름", // 화면에 노출될 앱의 한글 이름으로 바꿔주세요.
7
+ primaryColor: "{{PRIMARY_COLOR}}", // 화면에 노출될 앱의 기본 색상으로 바꿔주세요.
8
+ icon: "", // 화면에 노출될 앱의 아이콘 이미지 주소로 바꿔주세요.
9
+ },
10
+ web: {
11
+ host: "localhost",
12
+ port: 5173,
13
+ commands: {
14
+ dev: "vite dev",
15
+ build: "vite build",
16
+ },
17
+ },
18
+ permissions: [],
19
+ outdir: "dist",
20
+ });
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AIT App</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "granite dev",
8
+ "build": "ait build",
9
+ "deploy": "ait deploy",
10
+ "lint": "eslint .",
11
+ "format": "prettier --write ."
12
+ },
13
+ "dependencies": {
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@eslint/js": "^9.21.0",
19
+ "@vitejs/plugin-react": "^4.3.4",
20
+ "eslint": "^9.21.0",
21
+ "eslint-plugin-react-hooks": "^5.1.0",
22
+ "eslint-plugin-react-refresh": "^0.4.19",
23
+ "globals": "^15.15.0",
24
+ "prettier": "^3.4.2",
25
+ "vite": "^6.2.0"
26
+ }
27
+ }