From 811ff17a7958a867460b27467ed69d9aba525e62 Mon Sep 17 00:00:00 2001 From: atsumi Date: Fri, 26 Sep 2025 21:47:54 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20OpenAPI=20Generator=E3=82=92?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E3=81=97=E3=81=9F=E6=96=B0API=E4=BB=95?= =?UTF-8?q?=E6=A7=98=E3=81=B8=E3=81=AE=E7=A7=BB=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - サーバー側リポジトリからapi.yamlを取得して配置 - OpenAPI Generator Gradleプラグインを設定 - Kotlin + Retrofit2用のAPIクライアントコードを自動生成 - 既存のAPIServiceインターフェースを新しい実装に置き換え - Prod環境とDemo環境のDI設定を更新 - 古いAPI実装を完全に削除 Breaking Changes: - APIエンドポイントパスが変更(/api/プレフィックスが追加) - レスポンス構造の一部が変更 --- app/build.gradle | 34 + app/openapi/api.yaml | 1026 +++++++++++++++++ .../android/kidspos/di/module/apiModule.kt | 75 +- .../cuc/android/kidspos/api/APIService.kt | 122 +- .../android/kidspos/di/module/apiModule.kt | 26 +- build.gradle | 10 + 6 files changed, 1246 insertions(+), 47 deletions(-) create mode 100644 app/openapi/api.yaml diff --git a/app/build.gradle b/app/build.gradle index f13e4c8..67ee3d6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'com.google.dagger.hilt.android' + id 'org.openapi.generator' } android { @@ -133,7 +134,40 @@ dependencies { implementation libs.retrofit.client implementation libs.retrofit.converter.serialization + implementation "com.squareup.retrofit2:converter-scalars:2.9.0" implementation libs.logger implementation libs.eventbus } + +// OpenAPI Generator Configuration +openApiGenerate { + generatorName.set("kotlin") + inputSpec.set("$projectDir/openapi/api.yaml") + outputDir.set("$buildDir/generated/openapi") + apiPackage.set("info.nukoneko.cuc.android.kidspos.api.generated") + modelPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.model") + invokerPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.invoker") + configOptions.set([ + "library": "jvm-retrofit2", + "useCoroutines": "true", + "serializationLibrary": "kotlinx_serialization", + "dateLibrary": "java8", + "enumPropertyNaming": "UPPERCASE", + "collectionType": "list" + ]) + additionalProperties.set([ + "generateApiDocumentation": "false", + "generateModelDocumentation": "false" + ]) +} + +// Add generated sources to source sets +android.sourceSets { + main.kotlin.srcDirs += "$buildDir/generated/openapi/src/main/kotlin" +} + +// Make sure code is generated before compilation +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + dependsOn tasks.openApiGenerate +} diff --git a/app/openapi/api.yaml b/app/openapi/api.yaml new file mode 100644 index 0000000..8ef708e --- /dev/null +++ b/app/openapi/api.yaml @@ -0,0 +1,1026 @@ +openapi: 3.0.3 +info: + title: KidsPOS API + description: 子供向けPOSシステムのREST API + version: 1.0.0 + contact: + name: KidsPOS Team + url: https://github.com/KidsPOSProject/KidsPOS-Server + +servers: + - url: http://localhost:8080 + description: Local development server + - url: https://api.kidspos.example.com + description: Production server + +tags: + - name: Items + description: 商品管理 + - name: Sales + description: 売上管理 + - name: Staff + description: スタッフ管理 + - name: Stores + description: 店舗管理 + - name: Settings + description: 設定管理 + - name: Users + description: ユーザー管理 + +paths: + # Items + /api/item: + get: + tags: + - Items + summary: 商品一覧取得 + description: 登録されている全商品を取得します + operationId: getAllItems + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ItemResponse' + + post: + tags: + - Items + summary: 商品登録 + description: 新しい商品を登録します + operationId: createItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateItemRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '400': + description: Invalid request + '409': + description: Barcode already exists + + /api/item/{id}: + get: + tags: + - Items + summary: 商品取得 + description: 指定したIDの商品を取得します + operationId: getItemById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + put: + tags: + - Items + summary: 商品更新 + description: 指定したIDの商品を更新します + operationId: updateItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateItemRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + patch: + tags: + - Items + summary: 商品部分更新 + description: 指定したIDの商品を部分的に更新します + operationId: partialUpdateItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + barcode: + type: string + name: + type: string + price: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '404': + description: Item not found + + delete: + tags: + - Items + summary: 商品削除 + description: 指定したIDの商品を削除します + operationId: deleteItem + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + '404': + description: Item not found + + /api/item/barcode/{barcode}: + get: + tags: + - Items + summary: バーコードで商品取得 + description: バーコードから商品を検索します + operationId: getItemByBarcode + parameters: + - name: barcode + in: path + required: true + schema: + type: string + pattern: '^[0-9]{4,}$' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ItemResponse' + '400': + description: Invalid barcode format + '404': + description: Item not found + + /api/item/barcode-pdf: + get: + tags: + - Items + summary: バーコードPDF生成 + description: 全商品のバーコードをPDF形式で生成します + operationId: generateBarcodePdf + responses: + '200': + description: Success + content: + application/pdf: + schema: + type: string + format: binary + + # Sales + /api/sales: + get: + tags: + - Sales + summary: 売上一覧取得 + description: 全売上データを取得します + operationId: getAllSales + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SaleResponse' + + post: + tags: + - Sales + summary: 売上登録 + description: 新しい売上を登録します + operationId: createSale + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateSaleRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: string + saleId: + type: integer + totalAmount: + type: integer + change: + type: integer + receiptPrinted: + type: boolean + '400': + description: Validation error + '500': + description: Processing error + + /api/sales/{id}: + get: + tags: + - Sales + summary: 売上詳細取得 + description: 指定したIDの売上詳細を取得します + operationId: getSaleById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SaleResponse' + '404': + description: Sale not found + + /api/sales/validate-printer/{storeId}: + get: + tags: + - Sales + summary: プリンター設定確認 + description: 指定した店舗のプリンター設定を確認します + operationId: validatePrinter + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + isValid: + type: boolean + + # Staff + /api/staff: + get: + tags: + - Staff + summary: スタッフ一覧取得 + description: 全スタッフを取得します + operationId: getAllStaff + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StaffEntity' + + post: + tags: + - Staff + summary: スタッフ登録 + description: 新しいスタッフを登録します + operationId: createStaff + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateStaffRequest' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/StaffEntity' + + /api/staff/{barcode}: + get: + tags: + - Staff + summary: スタッフ取得 + description: バーコードからスタッフを取得します + operationId: getStaffByBarcode + parameters: + - name: barcode + in: path + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StaffEntity' + '404': + description: Staff not found + + put: + tags: + - Staff + summary: スタッフ更新 + description: スタッフ情報を更新します + operationId: updateStaff + parameters: + - name: barcode + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateStaffRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StaffEntity' + '404': + description: Staff not found + + delete: + tags: + - Staff + summary: スタッフ削除 + description: スタッフを削除します + operationId: deleteStaff + parameters: + - name: barcode + in: path + required: true + schema: + type: string + responses: + '204': + description: No Content + '404': + description: Staff not found + + # Stores + /api/stores: + get: + tags: + - Stores + summary: 店舗一覧取得 + description: 全店舗を取得します + operationId: getAllStores + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StoreEntity' + + post: + tags: + - Stores + summary: 店舗登録 + description: 新しい店舗を登録します + operationId: createStore + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + + /api/stores/{id}: + get: + tags: + - Stores + summary: 店舗取得 + description: 指定したIDの店舗を取得します + operationId: getStoreById + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + '404': + description: Store not found + + put: + tags: + - Stores + summary: 店舗更新 + description: 店舗情報を更新します + operationId: updateStore + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StoreEntity' + '404': + description: Store not found + + delete: + tags: + - Stores + summary: 店舗削除 + description: 店舗を削除します + operationId: deleteStore + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + '404': + description: Store not found + + # Settings + /api/setting: + get: + tags: + - Settings + summary: 設定一覧取得 + description: 全設定を取得します + operationId: getAllSettings + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SettingEntity' + + post: + tags: + - Settings + summary: 設定作成 + description: 新しい設定を作成します + operationId: createSetting + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + + /api/setting/status: + get: + tags: + - Settings + summary: ステータス取得 + description: APIのステータスを取得します + operationId: getStatus + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + status: + type: string + example: "OK" + + /api/setting/{key}: + get: + tags: + - Settings + summary: 設定取得 + description: 指定したキーの設定を取得します + operationId: getSettingByKey + parameters: + - name: key + in: path + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + '404': + description: Setting not found + + put: + tags: + - Settings + summary: 設定更新 + description: 設定を更新します + operationId: updateSetting + parameters: + - name: key + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + value: + type: string + required: + - value + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SettingEntity' + '404': + description: Setting not found + + delete: + tags: + - Settings + summary: 設定削除 + description: 設定を削除します + operationId: deleteSetting + parameters: + - name: key + in: path + required: true + schema: + type: string + responses: + '204': + description: No Content + '404': + description: Setting not found + + /api/setting/printer/{storeId}: + get: + tags: + - Settings + summary: プリンター設定取得 + description: 店舗のプリンター設定を取得します + operationId: getPrinterSettings + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + storeId: + type: integer + host: + type: string + port: + type: integer + '404': + description: Printer settings not found + + post: + tags: + - Settings + summary: プリンター設定保存 + description: 店舗のプリンター設定を保存します + operationId: savePrinterSettings + parameters: + - name: storeId + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + host: + type: string + port: + type: integer + required: + - host + - port + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + storeId: + type: integer + host: + type: string + port: + type: integer + message: + type: string + + /api/setting/application: + get: + tags: + - Settings + summary: アプリケーション設定取得 + description: アプリケーション全体の設定を取得します + operationId: getApplicationSettings + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationSetting' + '404': + description: Application settings not found + + post: + tags: + - Settings + summary: アプリケーション設定保存 + description: アプリケーション全体の設定を保存します + operationId: saveApplicationSettings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ApplicationSetting' + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + serverHost: + type: string + serverPort: + type: integer + message: + type: string + + # Users + /api/users: + get: + tags: + - Users + summary: ユーザー一覧取得 + description: 全ユーザーを取得します + operationId: getAllUsers + responses: + '200': + description: Success + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/StaffEntity' + + /api/users/{barcode}: + get: + tags: + - Users + summary: ユーザー取得 + description: バーコードからユーザーを取得します + operationId: getUserByBarcode + parameters: + - name: barcode + in: path + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/StaffEntity' + '404': + description: User not found + +components: + schemas: + ItemResponse: + type: object + properties: + id: + type: integer + example: 1 + barcode: + type: string + example: "1234567890" + name: + type: string + example: "ポテトチップス" + price: + type: integer + example: 150 + description: 価格(リバー) + required: + - id + - barcode + - name + - price + + CreateItemRequest: + type: object + properties: + barcode: + type: string + minLength: 4 + example: "1234567890" + name: + type: string + minLength: 1 + maxLength: 100 + example: "ポテトチップス" + price: + type: integer + minimum: 0 + maximum: 99999 + example: 150 + required: + - barcode + - name + - price + + SaleResponse: + type: object + properties: + id: + type: integer + storeId: + type: integer + staffBarcode: + type: string + createdAt: + type: string + format: date-time + totalAmount: + type: integer + description: 合計金額(リバー) + deposit: + type: integer + description: 預り金(リバー) + items: + type: array + items: + type: object + properties: + barcode: + type: string + name: + type: string + price: + type: integer + quantity: + type: integer + + CreateSaleRequest: + type: object + properties: + storeId: + type: integer + staffBarcode: + type: string + items: + type: array + items: + type: object + properties: + barcode: + type: string + quantity: + type: integer + minimum: 1 + required: + - barcode + - quantity + deposit: + type: integer + minimum: 0 + printReceipt: + type: boolean + default: true + required: + - storeId + - staffBarcode + - items + - deposit + + StaffEntity: + type: object + properties: + barcode: + type: string + example: "STAFF001" + name: + type: string + example: "田中太郎" + required: + - barcode + - name + + CreateStaffRequest: + type: object + properties: + barcode: + type: string + minLength: 1 + maxLength: 50 + name: + type: string + minLength: 1 + maxLength: 100 + required: + - barcode + - name + + UpdateStaffRequest: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 100 + required: + - name + + StoreEntity: + type: object + properties: + id: + type: integer + name: + type: string + printerUri: + type: string + nullable: true + required: + - name + + SettingEntity: + type: object + properties: + key: + type: string + value: + type: string + required: + - key + - value + + ApplicationSetting: + type: object + properties: + serverHost: + type: string + serverPort: + type: integer + required: + - serverHost + - serverPort + + ErrorResponse: + type: object + properties: + error: + type: string + message: + type: string + timestamp: + type: string + format: date-time + path: + type: string + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +# セキュリティは現在未実装のため、コメントアウト +# security: +# - bearerAuth: [] \ No newline at end of file diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt index 5529635..601bac8 100644 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt +++ b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt @@ -8,35 +8,58 @@ import info.nukoneko.cuc.android.kidspos.entity.Store import org.koin.dsl.module import java.util.* -val apiModule = module { - single { - object : APIService { - override suspend fun getStatus(): Any = Any() +/** + * Demo用のAPIService実装 + * ローカルで動作確認用のモックデータを返す + */ +class DemoAPIService : APIService( + itemsApi = throw NotImplementedError("Demo mode - itemsApi not used"), + salesApi = throw NotImplementedError("Demo mode - salesApi not used"), + staffApi = throw NotImplementedError("Demo mode - staffApi not used"), + storesApi = throw NotImplementedError("Demo mode - storesApi not used"), + settingsApi = throw NotImplementedError("Demo mode - settingsApi not used") +) { + override suspend fun getStatus(): Any = mapOf("status" to "OK", "mode" to "demo") + + override suspend fun createSale( + storeId: Int, + staffBarcode: String, + deposit: Int, + itemIds: String + ): Sale = Sale( + id = 1, + barcode = "123456", + createdAt = Date().toString(), + points = itemIds.split(",").size, + price = 100, + items = itemIds, + storeId = storeId, + staffId = 0 + ) - override suspend fun createSale( - storeId: Int, - staffBarcode: String, - deposit: Int, - itemIds: String - ): Sale = Sale( - 1, - "123456", - Date().toString(), - itemIds.split(",").size, - 0, - itemIds, - storeId, - 0 - ) + override suspend fun fetchStores(): List = + listOf( + Store(1, "お店1", null), + Store(2, "お店2", null) + ) - override suspend fun fetchStores(): List = - listOf(Store(1, "お店1"), Store(2, "お店2")) + override suspend fun getItem(itemBarcode: String): Item = + Item( + id = 1, + barcode = itemBarcode, + name = "DemoItem", + price = 100, + storeId = 1, + genreId = 1 + ) - override suspend fun getItem(itemBarcode: String): Item = - Item(1, itemBarcode, "DemoItem", 100, 1, 1) + override suspend fun getStaff(staffBarcode: String): Staff = + Staff(staffBarcode, "DemoStaff") +} - override suspend fun getStaff(staffBarcode: String): Staff = - Staff(staffBarcode, "DemoStaff") - } +val apiModule = module { + // Demo用のAPIServiceを登録 + single { + DemoAPIService() } } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt index 99bd1b1..b23e6e0 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/api/APIService.kt @@ -1,31 +1,115 @@ package info.nukoneko.cuc.android.kidspos.api +import info.nukoneko.cuc.android.kidspos.api.generated.* +import info.nukoneko.cuc.android.kidspos.api.generated.model.* import info.nukoneko.cuc.android.kidspos.entity.Item import info.nukoneko.cuc.android.kidspos.entity.Sale import info.nukoneko.cuc.android.kidspos.entity.Staff import info.nukoneko.cuc.android.kidspos.entity.Store -import retrofit2.http.* +import retrofit2.Response -interface APIService { +/** + * OpenAPI Generator で生成されたクライアントを使用するAPIService実装 + */ +open class APIService( + private val itemsApi: ItemsApi, + private val salesApi: SalesApi, + private val staffApi: StaffApi, + private val storesApi: StoresApi, + private val settingsApi: SettingsApi +) { - @GET("store/list") - suspend fun fetchStores(): List + open suspend fun fetchStores(): List { + val response = storesApi.getAllStores() + return if (response.isSuccessful) { + response.body()?.map { storeEntity -> + Store( + id = storeEntity.id ?: 0, + name = storeEntity.name ?: "", + printerUri = storeEntity.printerUri + ) + } ?: emptyList() + } else { + throw Exception("Failed to fetch stores: ${response.code()}") + } + } - @FormUrlEncoded - @POST("sale/create") - suspend fun createSale( - @Field("storeId") storeId: Int, - @Field("staffBarcode") staffBarcode: String, - @Field("deposit") deposit: Int, - @Field("itemIds") itemIds: String - ): Sale + open suspend fun createSale( + storeId: Int, + staffBarcode: String, + deposit: Int, + itemIds: String + ): Sale { + // itemIdsをカンマ区切りからリストに変換 + // 注意: 新しいAPIではitemIdではなくバーコードを使用 + val itemBarcodes = itemIds.split(",") - @GET("item/{barcode}") - suspend fun getItem(@Path("barcode") itemBarcode: String): Item + val request = CreateSaleRequest( + storeId = storeId, + staffBarcode = staffBarcode, + deposit = deposit, + items = itemBarcodes.map { barcode -> + CreateSaleRequestItemsInner( + barcode = barcode, + quantity = 1 // デフォルトで1個とする + ) + } + ) - @GET("staff/{barcode}") - suspend fun getStaff(@Path("barcode") staffBarcode: String): Staff + val response = salesApi.createSale(request) + return if (response.isSuccessful) { + val saleResponse = response.body()!! + Sale( + id = saleResponse.saleId ?: 0, + barcode = saleResponse.saleId?.toString() ?: "", // バーコードはIDから生成 + createdAt = java.util.Date().toString(), // 現在時刻を設定 + points = 0, // ポイントは新APIにない + price = saleResponse.totalAmount ?: 0, + items = itemIds, // 元のitemIdsをそのまま使用 + storeId = storeId, + staffId = 0 // スタッフIDは取得できないため仮値 + ) + } else { + throw Exception("Failed to create sale: ${response.code()}") + } + } - @GET("setting/status") - suspend fun getStatus(): Any -} + open suspend fun getItem(itemBarcode: String): Item { + val response = itemsApi.getItemByBarcode(itemBarcode) + return if (response.isSuccessful) { + val itemResponse = response.body()!! + Item( + id = itemResponse.id, + barcode = itemResponse.barcode, + name = itemResponse.name, + price = itemResponse.price, + storeId = 0, // 新APIではstoreIdが返ってこない + genreId = 0 // 新APIではgenreIdが返ってこない + ) + } else { + throw Exception("Failed to get item: ${response.code()}") + } + } + + open suspend fun getStaff(staffBarcode: String): Staff { + val response = staffApi.getStaffByBarcode(staffBarcode) + return if (response.isSuccessful) { + val staffResponse = response.body()!! + Staff( + barcode = staffResponse.barcode ?: staffBarcode, + name = staffResponse.name ?: "" + ) + } else { + throw Exception("Failed to get staff: ${response.code()}") + } + } + + open suspend fun getStatus(): Any { + val response = settingsApi.getStatus() + return if (response.isSuccessful) { + response.body() ?: mapOf() + } else { + throw Exception("Failed to get status: ${response.code()}") + } + } +} \ No newline at end of file diff --git a/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt b/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt index fdb426b..273f9ef 100644 --- a/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt +++ b/app/src/prod/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt @@ -1,11 +1,33 @@ package info.nukoneko.cuc.android.kidspos.di.module import info.nukoneko.cuc.android.kidspos.api.APIService +import info.nukoneko.cuc.android.kidspos.api.generated.* import org.koin.dsl.module import retrofit2.Retrofit val apiModule = module { - single { - (get()).create(APIService::class.java) + // 生成されたAPIインターフェース + single { + (get()).create(ItemsApi::class.java) + } + single { + (get()).create(SalesApi::class.java) + } + single { + (get()).create(StaffApi::class.java) + } + single { + (get()).create(StoresApi::class.java) + } + single { + (get()).create(SettingsApi::class.java) + } + single { + (get()).create(UsersApi::class.java) + } + + // APIServiceの実装 + single { + APIService(get(), get(), get(), get(), get()) } } diff --git a/build.gradle b/build.gradle index 2757db9..cb68c31 100644 --- a/build.gradle +++ b/build.gradle @@ -1,3 +1,13 @@ +buildscript { + repositories { + mavenLocal() + mavenCentral() + } + dependencies { + classpath "org.openapitools:openapi-generator-gradle-plugin:7.2.0" + } +} + plugins { alias libs.plugins.android.application apply false alias libs.plugins.android.library apply false From 94a86a15a7d135cddcad62e8f60f51a021f72eee Mon Sep 17 00:00:00 2001 From: atsumi Date: Fri, 26 Sep 2025 21:50:51 +0900 Subject: [PATCH 2/3] =?UTF-8?q?chore:=20AI=E9=96=8B=E7=99=BA=E6=94=AF?= =?UTF-8?q?=E6=8F=B4=E3=83=84=E3=83=BC=E3=83=AB=E3=81=AE=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Claude Code用の設定ファイル(.claude/settings.json) - エディタ設定(.editorconfig) - MCP(Model Context Protocol)設定(.mcp.json) - プロジェクト用Claude開発ガイドライン(CLAUDE.md) これらのファイルにより、AI支援開発の効率化と一貫性が向上します。 --- .claude/settings.json | 66 +++++++++++++++++++++++++++ .editorconfig | 4 ++ .mcp.json | 48 ++++++++++++++++++++ CLAUDE.md | 103 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .editorconfig create mode 100644 .mcp.json create mode 100644 CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..70195fa --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "includeCoAuthoredBy": false, + "enableAllProjectMcpServers": true, + "permissions": { + "allow": [ + "Bash(gemini:*)", + "Bash(git:*)" + ], + "deny": [ + "Bash(sudo:*)", + "Read(.env.*)", + "Read(id_rsa)", + "Read(id_ed25519)", + "Read(**/*token*)", + "Read(**/*key*)", + "Write(.env*)", + "Write(**/secrets/**)", + "Bash(wget:*)", + "Bash(nc:*)", + "Bash(rm:*)", + "Bash(npm uninstall:*)", + "Bash(npm remove:*)", + "Bash(psql:*)", + "Bash(mysql:*)", + "Bash(mongod:*)", + "mcp__supabase__execute_sql" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "if jq -r '.tool_input.command' | grep -q '^git commit' && jq -r '.tool_input.command' | grep -q '🤖 Generated with'; then echo 'Error: コミットメッセージに AI 署名が含まれている' 1>&2; exit 2; fi" + } + ] + } + ], + "PostToolUse": [], + "Notification": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"確認待ち\" with title \"Claude Code\"'" + } + ] + } + ], + "Stop": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "osascript -e 'display notification \"タスク完了\" with title \"Claude Code\"'" + } + ] + } + ] + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1b60f1e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ktlint_code_style = ktlint_official +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_filename = disabled diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..da39ea4 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,48 @@ +{ + "mcpServers": { + "serena": { + "type": "stdio", + "command": "uvx", + "args": [ + "--from", + "git+https://github.com/oraios/serena", + "serena-mcp-server", + "--context", + "ide-assistant", + "--project", + "." + ], + "env": {} + }, + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + }, + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + }, + "context7": { + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp" + ] + } + }, + "scopes": { + "ide-assistant": { + "mcpServers": [ + "serena", + "playwright", + "sequential-thinking", + "context7" + ] + } + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..16cf8f6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## プロジェクト情報 +- **名称**: KidsPOS for Android - キッズビジネスタウンいちかわ用POSシステム +- **パッケージ**: info.nukoneko.cuc.android.kidspos +- **Android SDK**: minSdk 23, targetSdk 33 (buildSrc/Versions.ktで管理) +- **Kotlin JVM Target**: 17 + +## 開発コマンド + +### ビルド +```bash +# Prodビルド(本番用) +./gradlew assembleProdDebug # デバッグビルド +./gradlew assembleProdRelease # リリースビルド + +# Demoビルド(デモ用) +./gradlew assembleDemoDebug +./gradlew assembleDemoRelease + +# クリーンビルド +./gradlew clean assembleProdDebug +``` + +### テストとLint +```bash +# ユニットテスト実行 +./gradlew test +./gradlew testProdDebugUnitTest + +# Lint実行(必須) +./gradlew lint +./gradlew lintProdDebug + +# Lint自動修正 +./gradlew lintFix +``` + +### デバイスへのインストール +```bash +./gradlew installProdDebug +./gradlew uninstallProdDebug +``` + +## アーキテクチャ構造 + +### MVVMパターン +- **View層**: Activity/Fragment (DataBinding/ViewBinding使用) +- **ViewModel層**: AndroidX ViewModelを継承 +- **Model層**: Repository、API、Entity + +### 依存性注入 +- **Koin**: メインのDIフレームワーク(modules: coreModule, apiModule, viewModelModule) +- **Hilt**: Dagger Hiltも併用可能 +- 新規依存は `di/module/` 配下の適切なモジュールに追加 + +### イベント駆動 +- **EventBus** (org.greenrobot): アプリ全体のイベント通信 +- イベントクラスは `event/` パッケージに配置 +- `@Subscribe(threadMode = ThreadMode.MAIN)` でハンドラー定義 + +### 非同期処理 +- **Kotlin Coroutines**: `viewModelScope`、`lifecycleScope` を使用 +- **Retrofit + OkHttp**: API通信(ServerSelectionInterceptorで動的サーバー切替) + +## コーディング規則 + +### ログ出力(重要) +```kotlin +// ❌ 禁止 +print("message") +println("message") +debugPrint("message") + +// ✅ 必須:Logger使用 +Logger.d("Debug message") +Logger.e("Error message") +Logger.i("Info message") +``` + +### ファイル命名 +- Activity: `*Activity.kt` +- Fragment: `*Fragment.kt` +- ViewModel: `*ViewModel.kt` +- レイアウト: `activity_*.xml`, `fragment_*.xml`, `item_*.xml` + +### バーコード機能 +- `BarcodeReadDelegate` インターフェースを実装 +- `BaseBarcodeReadableActivity` を継承してActivity作成 + +### リソース管理 +- 全ての文字列は `strings.xml` に定義(日本語のみ) +- 既存実装パターンを必ず参照・踏襲する + +## タスク完了時の確認事項 + +1. **ビルド成功確認**: `./gradlew assembleProdDebug` +2. **Lintエラーゼロ**: `./gradlew lint` +3. **テスト合格**: `./gradlew test` +4. **Logger使用確認**: print文が含まれていないこと +5. **既存パターン準拠**: 類似機能の実装を参照したこと From 168123644d15cdf16544da0cc7a1256b09378040 Mon Sep 17 00:00:00 2001 From: atsumi Date: Sat, 27 Sep 2025 00:40:30 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20OpenAPI=E7=A7=BB=E8=A1=8C=E5=BE=8C?= =?UTF-8?q?=E3=81=AE=E3=83=93=E3=83=AB=E3=83=89=E3=82=A8=E3=83=A9=E3=83=BC?= =?UTF-8?q?=E3=81=A8=E6=A9=9F=E8=83=BD=E6=94=B9=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 修正内容 ### ビルドエラー修正 - APIServiceのDI定義で型指定を追加 - ZXingライブラリのtransitive設定を削除(依存関係の不足を解消) - Lintエラー2件を修正 - StoreListDialogFragment: context!!をrequireContext()に変更 - AndroidManifest: カメラのuses-featureタグを追加 ### UI/UX改善 - 設定画面のActionBarに戻るボタンを追加 - お店切り替え画面にタイムアウト処理を追加(3秒) - 練習モード用のダミー店舗を「100リバー」「デパート」に設定 - 練習モードで商品追加ボタンを実装(ダミー商品を追加可能) - ボタンのパディングを統一 ### 安定性向上 - DialogFragmentの状態チェックを追加し、onSaveInstanceState後のクラッシュを防止 --- app/build.gradle | 26 ++++++++--------- .../android/kidspos/di/module/apiModule.kt | 2 +- app/src/main/AndroidManifest.xml | 3 ++ .../kidspos/ui/common/ErrorDialogFragment.kt | 16 +++++++++-- .../calculate/AccountResultDialogFragment.kt | 15 ++++++++-- .../ui/main/itemlist/ItemListViewModel.kt | 23 +++++++++++++++ .../main/storelist/StoreListDialogFragment.kt | 4 +-- .../ui/main/storelist/StoreListViewModel.kt | 28 +++++++++++++++++-- .../kidspos/ui/setting/SettingActivity.kt | 10 +++++++ .../main/res/layout/fragment_item_list.xml | 17 +++++++++++ app/src/main/res/values/strings.xml | 2 ++ .../android/kidspos/di/module/apiModule.kt | 10 +++++-- 12 files changed, 132 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 67ee3d6..e9b0320 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ import dependencies.Dep import dependencies.Packages import dependencies.Versions +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id 'com.android.application' @@ -118,9 +119,7 @@ dependencies { implementation libs.androidx.preference.ktx implementation libs.androidx.constraintlayout - implementation(Dep.Zxing.android) { - transitive = false - } + implementation(Dep.Zxing.android) coreLibraryDesugaring libs.desugar.jdk.libs @@ -134,7 +133,7 @@ dependencies { implementation libs.retrofit.client implementation libs.retrofit.converter.serialization - implementation "com.squareup.retrofit2:converter-scalars:2.9.0" + implementation libs.converter.scalars implementation libs.logger implementation libs.eventbus @@ -149,16 +148,16 @@ openApiGenerate { modelPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.model") invokerPackage.set("info.nukoneko.cuc.android.kidspos.api.generated.invoker") configOptions.set([ - "library": "jvm-retrofit2", - "useCoroutines": "true", - "serializationLibrary": "kotlinx_serialization", - "dateLibrary": "java8", - "enumPropertyNaming": "UPPERCASE", - "collectionType": "list" + "library" : "jvm-retrofit2", + "useCoroutines" : "true", + "serializationLibrary": "kotlinx_serialization", + "dateLibrary" : "java8", + "enumPropertyNaming" : "UPPERCASE", + "collectionType" : "list" ]) additionalProperties.set([ - "generateApiDocumentation": "false", - "generateModelDocumentation": "false" + "generateApiDocumentation" : "false", + "generateModelDocumentation": "false" ]) } @@ -168,6 +167,7 @@ android.sourceSets { } // Make sure code is generated before compilation -tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { +tasks.withType(KotlinCompile).configureEach { dependsOn tasks.openApiGenerate } + diff --git a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt index 601bac8..cd0ff66 100644 --- a/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt +++ b/app/src/demo/kotlin/info/nukoneko/cuc/android/kidspos/di/module/apiModule.kt @@ -59,7 +59,7 @@ class DemoAPIService : APIService( val apiModule = module { // Demo用のAPIServiceを登録 - single { + single { DemoAPIService() } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index faf6a9e..2a541d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + + () fun getAccountButtonEnabled(): LiveData = accountButtonEnabled + private val addItemButtonVisibility = MutableLiveData() + fun getAddItemButtonVisibility(): LiveData = addItemButtonVisibility + var listener: Listener? = null private val data: MutableList = mutableListOf() @@ -51,6 +55,21 @@ class ItemListViewModel( listener?.onStartAccount(data) } + fun onClickAddItem(@Suppress("UNUSED_PARAMETER") view: View?) { + // ダミー商品を追加 + val dummyItems = listOf( + Item(1, "123456789", "おもちゃ", 100, 1, 1), + Item(2, "223456789", "お菓子", 50, 1, 1), + Item(3, "323456789", "本", 200, 1, 1), + Item(4, "423456789", "文房具", 150, 1, 1), + Item(5, "523456789", "ゲーム", 300, 1, 1) + ) + val randomItem = dummyItems.random() + data.add(randomItem) + listener?.onDataAdded(randomItem) + updateViews() + } + fun onResume() { updateViews() } @@ -69,6 +88,10 @@ class ItemListViewModel( currentStaffVisibility.value = if (config.currentStaff == null) View.INVISIBLE else View.VISIBLE currentStaff.postValue("たんとう: ${config.currentStaff?.name ?: ""}") + // 練習モードのときのみ商品追加ボタンを表示 + addItemButtonVisibility.postValue( + if (config.currentRunningMode == Mode.PRACTICE) View.VISIBLE else View.GONE + ) } @Subscribe(threadMode = ThreadMode.MAIN) diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt index 7284676..32d8999 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListDialogFragment.kt @@ -71,11 +71,11 @@ class StoreListDialogFragment : DialogFragment() { binding.recyclerView.adapter = adapter binding.recyclerView.addItemDecoration( DividerItemDecoration( - context!!, + requireContext(), DividerItemDecoration.VERTICAL ) ) - binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) } private fun setupSubscriber() { diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewModel.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewModel.kt index fe395ee..7ef4a04 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewModel.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/main/storelist/StoreListViewModel.kt @@ -8,10 +8,12 @@ import info.nukoneko.cuc.android.kidspos.api.APIService import info.nukoneko.cuc.android.kidspos.api.RequestStatus import info.nukoneko.cuc.android.kidspos.di.GlobalConfig import info.nukoneko.cuc.android.kidspos.entity.Store +import info.nukoneko.cuc.android.kidspos.util.Mode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout import kotlin.coroutines.CoroutineContext class StoreListViewModel( @@ -90,10 +92,24 @@ class StoreListViewModel( if (requestStatus == RequestStatus.REQUESTING) { return } + + // 練習モードの場合はダミー店舗を即座に表示 + if (config.currentRunningMode == Mode.PRACTICE) { + val dummyStores = listOf( + Store(1, "100リバー", null), + Store(2, "デパート", null) + ) + onFetchStoresSuccess(dummyStores) + return + } + requestStatus = RequestStatus.REQUESTING launch { requestStatus = try { - val stores: List = requestFetchStores() + // 3秒のタイムアウトを設定 + val stores: List = withTimeout(3000L) { + requestFetchStores() + } onFetchStoresSuccess(stores) RequestStatus.SUCCESS } catch (e: Throwable) { @@ -118,7 +134,15 @@ class StoreListViewModel( } private fun onFetchStoresFailure(error: Throwable) { - error.localizedMessage?.let { listener?.onShouldShowErrorDialog(it) } + val message = when { + error is kotlinx.coroutines.TimeoutCancellationException -> + "サーバーとの通信がタイムアウトしました。\n接続を確認してください。" + error.localizedMessage != null -> + error.localizedMessage!! + else -> + "通信エラーが発生しました。" + } + listener?.onShouldShowErrorDialog(message) requestStatus = RequestStatus.FAILURE } diff --git a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt index cd3149f..061fa49 100644 --- a/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt +++ b/app/src/main/kotlin/info/nukoneko/cuc/android/kidspos/ui/setting/SettingActivity.kt @@ -15,11 +15,21 @@ class SettingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // ActionBarに戻るボタンを表示 + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.title = "設定" + supportFragmentManager.beginTransaction() .replace(android.R.id.content, SettingFragment.newInstance()) .commit() } + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null) { diff --git a/app/src/main/res/layout/fragment_item_list.xml b/app/src/main/res/layout/fragment_item_list.xml index bf19bb1..6dcdf66 100644 --- a/app/src/main/res/layout/fragment_item_list.xml +++ b/app/src/main/res/layout/fragment_item_list.xml @@ -43,10 +43,27 @@ android:background="@drawable/layout_button_rect_red" android:maxLines="1" android:onClick="@{viewModel::onClickClear}" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:text="@string/delete" android:textColor="@android:color/white" android:textSize="22sp" /> +