diff --git a/.gitignore b/.gitignore
index f611f55..c7383fd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,3 +37,5 @@ out/
.vscode/
src/main/resources/static
+
+.DS_Store
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..8aa2645
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [year] [fullname]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README-ja.md b/README-ja.md
new file mode 100644
index 0000000..cb6239f
--- /dev/null
+++ b/README-ja.md
@@ -0,0 +1,50 @@
+# oi - a card game
+
+## Build and run
+
+```shell
+cd front
+npm i
+cd ..
+./gradlew buildFront clean bootJar
+java -jar build/libs/oi-0.0.1-SNAPSHOT.jar
+```
+
+Access to http://localhost:8080/
+
+## How to play oi
+
+demo: https://oi.moreslowly.jp/
+
+## 遊び方
+
+### ゲームの目的
+
+Oi はおいちょかぶに似たカードゲームです。それはトランプを使用したシンプルなカードゲームです。プレイヤーは手札のカードの合計値の一桁目で競います。最も強い手札を持つプレイヤーが勝者となり、賭けた金額を獲得します。
+
+### 準備
+
+- 1 〜 6 人のプレイヤー
+
+### ゲームの進行
+
+1. **手札の配布**: ゲームが開始されると、各プレイヤーに2枚のカードが配られます。1枚は裏向き(見えない)、もう1枚は表向き(見える)となります。
+
+2. **賭け金の設定**: プレイヤーは手札の強さに自信があると感じた場合、任意の賭け金を設定します。
+
+3. **カードの公開**: 賭け金を設定したプレイヤーは、裏向きになっていたカードをめくり、手札の2枚を確認します。
+
+4. **手札の強さの計算**: 手札の2枚のカードを足し合わせます。このとき、Jは11、Qは12、Kは13として計算し、数字のカードはそのままの数値で計算します。スートは関係ありません。手札の強さは、カードの合計値の一桁目によって決まります。9が最も強く、8, 7, 6, 5, 4, 3, 2, 1, 0 の順に弱くなります。
+
+5. **追加カードの要求**: プレイヤーは、手札の強さに満足がいかない場合、もう1枚のカードを要求することができます。この場合、3枚のカードの合計値の一桁目で手札の強さを計算します。
+
+6. **コンピュータプレイヤーの手札**: プレイヤーが手札の強さを決定した後、コンピュータプレイヤーが親として自分の手札を構成し、強さを決定します。
+
+7. **勝敗の判定**: プレイヤーの手札の強さとコンピュータプレイヤーの手札の強さを比較します。プレイヤーの方が強ければ、賭けた金額
+
+がプレイヤーに支払われます。
+
+### 注意点
+
+- 合計値の一桁目が同じ場合、引き分けとなります。
+- プレイヤーは追加カードを要求するかどうか慎重に考えましょう。3枚目のカードによって、手札の強さが変わります。
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3b3280c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,46 @@
+# oi - a card game
+
+## Build and run
+
+```shell
+cd front
+npm i
+cd ..
+./gradlew buildFront clean bootJar
+java -jar build/libs/oi-0.0.1-SNAPSHOT.jar
+```
+
+Access to http://localhost:8080/
+
+## How to play oi
+
+demo: https://oi.moreslowly.jp/
+
+### Objective of the Game
+
+Oi is a Oicho-Kabu like card game. It is a simple card game that uses a standard deck of playing cards. Players compete by calculating the units digit of the sum of their cards. The player with the strongest hand wins the bet.
+
+### Preparation
+
+- 1 to 6 players
+
+### Game Progression
+
+1. **Dealing the Cards**: At the start of the game, each player is dealt two cards. One card is face down (hidden), and the other is face up (visible).
+
+2. **Setting the Bet**: Players place bets if they feel confident about the strength of their hand.
+
+3. **Revealing the Card**: Players who have placed bets will turn over their face-down card, allowing them to see both of their cards.
+
+4. **Calculating Hand Strength**: Add the values of the two cards in your hand. For this calculation, J is 11, Q is 12, and K is 13, while numerical cards hold their face value. Suits do not matter. The strength of a hand is determined by the units digit of the sum of the cards. 9 is the strongest, followed by 8, 7, 6, 5, 4, 3, 2, 1, and 0.
+
+5. **Requesting an Additional Card**: If a player is not satisfied with the strength of their hand, they may request one additional card. In this case, the hand strength is calculated using the units digit of the sum of the three cards.
+
+6. **Computer Player’s Hand**: After the player has determined the strength of their hand, the computer player, acting as the dealer, constructs its hand and determines its strength.
+
+7. **Determining the Winner**: Compare the strength of the player’s hand with the strength of the computer player’s hand. If the player’s hand is stronger, they are paid the amount of the bet.
+
+### Points to Note
+
+- If the units digit of the sum is the same for both players, it is considered a draw.
+- Players should carefully consider whether or not to request an additional card. The third card can alter the strength of their hand.
diff --git a/build.gradle b/build.gradle
index 6faeba1..ed0d4c3 100644
--- a/build.gradle
+++ b/build.gradle
@@ -19,6 +19,7 @@ repositories {
}
dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-quartz'
implementation 'org.springframework.boot:spring-boot-starter-web'
@@ -28,6 +29,8 @@ dependencies {
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
+
+ implementation group: 'ch.qos.logback', name: 'logback-access', version: '1.4.6'
}
tasks.named('test') {
diff --git a/docs/name.md b/docs/name.md
new file mode 100644
index 0000000..21a93ef
--- /dev/null
+++ b/docs/name.md
@@ -0,0 +1,14 @@
+# names
+
+サクラ
+ウメ
+カエデ
+シラカバ
+アジサイ
+ツツジ
+モミジ
+ツバキ
+マツ
+モクレン
+タケ
+ヒイラギ
diff --git a/docs/spec.md b/docs/spec.md
new file mode 100644
index 0000000..eec8356
--- /dev/null
+++ b/docs/spec.md
@@ -0,0 +1,21 @@
+# spec
+
+入口 `https://oi.moreslowly.jp/`
+
+名前を入力
+セッションに保存
+名前の表示
+名前の変更
+
+`/?room=UUID`
+
+- 開始 start
+- シャッフル shuffle
+- カードを配る hand out cards
+- 掛け金設定 wait to bet
+- 全員OK
+- 3枚目のカードの要否 wait to request additional card
+- 全員OK
+- 親が自分のカードをめくる parent turns over cards
+- 親が三枚目のカードを取るか決める parent hand is determined
+- 清算 liquidation
diff --git a/front/index.html b/front/index.html
index 143557b..ce407e7 100644
--- a/front/index.html
+++ b/front/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + Vue + TS
+ Oi
diff --git a/front/package-lock.json b/front/package-lock.json
index c67c8e6..bfe5adc 100644
--- a/front/package-lock.json
+++ b/front/package-lock.json
@@ -1,565 +1,1135 @@
{
"name": "oi",
"version": "0.0.0",
- "lockfileVersion": 1,
+ "lockfileVersion": 3,
"requires": true,
- "dependencies": {
- "@babel/parser": {
- "version": "7.21.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.3.tgz",
- "integrity": "sha512-lobG0d7aOfQRXh8AyklEAgZGvA4FShxo6xQbUrrT/cNBPUdIDojlokwJsQyCC/eKia7ifqM0yP+2DRZ4WKw2RQ=="
+ "packages": {
+ "": {
+ "name": "oi",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.3.4",
+ "qs": "^6.11.1",
+ "uuid": "^9.0.0",
+ "vue": "^3.2.47",
+ "vue-router": "^4.1.6"
+ },
+ "devDependencies": {
+ "@types/qs": "^6.9.7",
+ "@types/uuid": "^9.0.1",
+ "@vitejs/plugin-vue": "^4.1.0",
+ "typescript": "^4.9.3",
+ "vite": "^4.2.3",
+ "vue-tsc": "^1.2.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.22.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz",
+ "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==",
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
},
- "@esbuild/android-arm": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.12.tgz",
- "integrity": "sha512-E/sgkvwoIfj4aMAPL2e35VnUJspzVYl7+M1B2cqeubdBhADV4uPon0KCc8p2G+LqSJ6i8ocYPCqY3A4GGq0zkQ==",
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/android-arm64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.12.tgz",
- "integrity": "sha512-WQ9p5oiXXYJ33F2EkE3r0FRDFVpEdcDiwNX3u7Xaibxfx6vQE0Sb8ytrfQsA5WO6kDn6mDfKLh6KrPBjvkk7xA==",
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/android-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.12.tgz",
- "integrity": "sha512-m4OsaCr5gT+se25rFPHKQXARMyAehHTQAz4XX1Vk3d27VtqiX0ALMBPoXZsGaB6JYryCLfgGwUslMqTfqeLU0w==",
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/darwin-arm64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.12.tgz",
- "integrity": "sha512-O3GCZghRIx+RAN0NDPhyyhRgwa19MoKlzGonIb5hgTj78krqp9XZbYCvFr9N1eUxg0ZQEpiiZ4QvsOQwBpP+lg==",
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/darwin-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.12.tgz",
- "integrity": "sha512-5D48jM3tW27h1qjaD9UNRuN+4v0zvksqZSPZqeSWggfMlsVdAhH3pwSfQIFJwcs9QJ9BRibPS4ViZgs3d2wsCA==",
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/freebsd-arm64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.12.tgz",
- "integrity": "sha512-OWvHzmLNTdF1erSvrfoEBGlN94IE6vCEaGEkEH29uo/VoONqPnoDFfShi41Ew+yKimx4vrmmAJEGNoyyP+OgOQ==",
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/freebsd-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.12.tgz",
- "integrity": "sha512-A0Xg5CZv8MU9xh4a+7NUpi5VHBKh1RaGJKqjxe4KG87X+mTjDE6ZvlJqpWoeJxgfXHT7IMP9tDFu7IZ03OtJAw==",
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-arm": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.12.tgz",
- "integrity": "sha512-WsHyJ7b7vzHdJ1fv67Yf++2dz3D726oO3QCu8iNYik4fb5YuuReOI9OtA+n7Mk0xyQivNTPbl181s+5oZ38gyA==",
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
+ "cpu": [
+ "arm"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-arm64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.12.tgz",
- "integrity": "sha512-cK3AjkEc+8v8YG02hYLQIQlOznW+v9N+OI9BAFuyqkfQFR+DnDLhEM5N8QRxAUz99cJTo1rLNXqRrvY15gbQUg==",
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-ia32": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.12.tgz",
- "integrity": "sha512-jdOBXJqcgHlah/nYHnj3Hrnl9l63RjtQ4vn9+bohjQPI2QafASB5MtHAoEv0JQHVb/xYQTFOeuHnNYE1zF7tYw==",
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-loong64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.12.tgz",
- "integrity": "sha512-GTOEtj8h9qPKXCyiBBnHconSCV9LwFyx/gv3Phw0pa25qPYjVuuGZ4Dk14bGCfGX3qKF0+ceeQvwmtI+aYBbVA==",
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
+ "cpu": [
+ "loong64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-mips64el": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.12.tgz",
- "integrity": "sha512-o8CIhfBwKcxmEENOH9RwmUejs5jFiNoDw7YgS0EJTF6kgPgcqLFjgoc5kDey5cMHRVCIWc6kK2ShUePOcc7RbA==",
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
+ "cpu": [
+ "mips64el"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-ppc64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.12.tgz",
- "integrity": "sha512-biMLH6NR/GR4z+ap0oJYb877LdBpGac8KfZoEnDiBKd7MD/xt8eaw1SFfYRUeMVx519kVkAOL2GExdFmYnZx3A==",
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
+ "cpu": [
+ "ppc64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-riscv64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.12.tgz",
- "integrity": "sha512-jkphYUiO38wZGeWlfIBMB72auOllNA2sLfiZPGDtOBb1ELN8lmqBrlMiucgL8awBw1zBXN69PmZM6g4yTX84TA==",
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
+ "cpu": [
+ "riscv64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-s390x": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.12.tgz",
- "integrity": "sha512-j3ucLdeY9HBcvODhCY4b+Ds3hWGO8t+SAidtmWu/ukfLLG/oYDMaA+dnugTVAg5fnUOGNbIYL9TOjhWgQB8W5g==",
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
+ "cpu": [
+ "s390x"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/linux-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.12.tgz",
- "integrity": "sha512-uo5JL3cgaEGotaqSaJdRfFNSCUJOIliKLnDGWaVCgIKkHxwhYMm95pfMbWZ9l7GeW9kDg0tSxcy9NYdEtjwwmA==",
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/netbsd-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.12.tgz",
- "integrity": "sha512-DNdoRg8JX+gGsbqt2gPgkgb00mqOgOO27KnrWZtdABl6yWTST30aibGJ6geBq3WM2TIeW6COs5AScnC7GwtGPg==",
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/openbsd-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.12.tgz",
- "integrity": "sha512-aVsENlr7B64w8I1lhHShND5o8cW6sB9n9MUtLumFlPhG3elhNWtE7M1TFpj3m7lT3sKQUMkGFjTQBrvDDO1YWA==",
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/sunos-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.12.tgz",
- "integrity": "sha512-qbHGVQdKSwi0JQJuZznS4SyY27tYXYF0mrgthbxXrZI3AHKuRvU+Eqbg/F0rmLDpW/jkIZBlCO1XfHUBMNJ1pg==",
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/win32-arm64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.12.tgz",
- "integrity": "sha512-zsCp8Ql+96xXTVTmm6ffvoTSZSV2B/LzzkUXAY33F/76EajNw1m+jZ9zPfNJlJ3Rh4EzOszNDHsmG/fZOhtqDg==",
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/win32-ia32": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.12.tgz",
- "integrity": "sha512-FfrFjR4id7wcFYOdqbDfDET3tjxCozUgbqdkOABsSFzoZGFC92UK7mg4JKRc/B3NNEf1s2WHxJ7VfTdVDPN3ng==",
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
+ "cpu": [
+ "ia32"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
},
- "@esbuild/win32-x64": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.12.tgz",
- "integrity": "sha512-JOOxw49BVZx2/5tW3FqkdjSD/5gXYeVGPDcB0lvap0gLQshkh1Nyel1QazC+wNxus3xPlsYAgqU1BUmrmCvWtw==",
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
- "optional": true
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.15",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
+ "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
- "@vitejs/plugin-vue": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.1.0.tgz",
- "integrity": "sha512-++9JOAFdcXI3lyer9UKUV4rfoQ3T1RN8yDqoCLar86s0xQct5yblxAE+yWgRnU5/0FOlVCpTZpYSBV/bGWrSrQ==",
+ "node_modules/@types/qs": {
+ "version": "6.9.7",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
+ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
- "@volar/language-core": {
- "version": "1.3.0-alpha.0",
- "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.3.0-alpha.0.tgz",
- "integrity": "sha512-W3uMzecHPcbwddPu4SJpUcPakRBK/y/BP+U0U6NiPpUX1tONLC4yCawt+QBJqtgJ+sfD6ztf5PyvPL3hQRqfOA==",
- "dev": true,
- "requires": {
- "@volar/source-map": "1.3.0-alpha.0"
- }
+ "node_modules/@types/uuid": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.2.tgz",
+ "integrity": "sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ==",
+ "dev": true
},
- "@volar/source-map": {
- "version": "1.3.0-alpha.0",
- "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.3.0-alpha.0.tgz",
- "integrity": "sha512-jSdizxWFvDTvkPYZnO6ew3sBZUnS0abKCbuopkc0JrIlFbznWC/fPH3iPFIMS8/IIkRxq1Jh9VVG60SmtsdaMQ==",
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
+ "integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
"dev": true,
- "requires": {
- "muggle-string": "^0.2.2"
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.0.0",
+ "vue": "^3.2.25"
}
},
- "@volar/typescript": {
- "version": "1.3.0-alpha.0",
- "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.3.0-alpha.0.tgz",
- "integrity": "sha512-5UItyW2cdH2mBLu4RrECRNJRgtvvzKrSCn2y3v/D61QwIDkGx4aeil6x8RFuUL5TFtV6QvVHXnsOHxNgd+sCow==",
+ "node_modules/@volar/language-core": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.7.6.tgz",
+ "integrity": "sha512-r+82YGjae8ALzaX+TaESpeBOrp/H5MQnPYZLq4WKd8rsPrCAPbMwelwHLHhFpyjy66BK/cKreJAcvOc6YEwyFA==",
"dev": true,
- "requires": {
- "@volar/language-core": "1.3.0-alpha.0"
+ "dependencies": {
+ "@volar/source-map": "1.7.6"
}
},
- "@volar/vue-language-core": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.2.0.tgz",
- "integrity": "sha512-w7yEiaITh2WzKe6u8ZdeLKCUz43wdmY/OqAmsB/PGDvvhTcVhCJ6f0W/RprZL1IhqH8wALoWiwEh/Wer7ZviMQ==",
- "dev": true,
- "requires": {
- "@volar/language-core": "1.3.0-alpha.0",
- "@volar/source-map": "1.3.0-alpha.0",
- "@vue/compiler-dom": "^3.2.47",
- "@vue/compiler-sfc": "^3.2.47",
- "@vue/reactivity": "^3.2.47",
- "@vue/shared": "^3.2.47",
- "minimatch": "^6.1.6",
- "muggle-string": "^0.2.2",
- "vue-template-compiler": "^2.7.14"
+ "node_modules/@volar/source-map": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.7.6.tgz",
+ "integrity": "sha512-6oGrgz+hg5GCzP8D2+ay7vOdIOA9/aXwpa22Wx5b6d4ZGwwosBqv7kVs8AyMh5zOSQpKhrImE1pfagpu+V+rBQ==",
+ "dev": true,
+ "dependencies": {
+ "muggle-string": "^0.3.1"
}
},
- "@volar/vue-typescript": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@volar/vue-typescript/-/vue-typescript-1.2.0.tgz",
- "integrity": "sha512-zjmRi9y3J1EkG+pfuHp8IbHmibihrKK485cfzsHjiuvJMGrpkWvlO5WVEk8oslMxxeGC5XwBFE9AOlvh378EPA==",
+ "node_modules/@volar/typescript": {
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.7.6.tgz",
+ "integrity": "sha512-JkBRQe2GYSEgamW84tDk4XQ/7abQJw09czLQCgL1jfjndhaV4DuAet2I3pvQv41OjodVc59W0+E3hylrlNsgWA==",
"dev": true,
- "requires": {
- "@volar/typescript": "1.3.0-alpha.0",
- "@volar/vue-language-core": "1.2.0"
+ "dependencies": {
+ "@volar/language-core": "1.7.6"
}
},
- "@vue/compiler-core": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.47.tgz",
- "integrity": "sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/shared": "3.2.47",
+ "node_modules/@vue/compiler-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
+ "integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
+ "dependencies": {
+ "@babel/parser": "^7.21.3",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
- "source-map": "^0.6.1"
- }
- },
- "@vue/compiler-dom": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz",
- "integrity": "sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==",
- "requires": {
- "@vue/compiler-core": "3.2.47",
- "@vue/shared": "3.2.47"
- }
- },
- "@vue/compiler-sfc": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz",
- "integrity": "sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.47",
- "@vue/compiler-dom": "3.2.47",
- "@vue/compiler-ssr": "3.2.47",
- "@vue/reactivity-transform": "3.2.47",
- "@vue/shared": "3.2.47",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
+ "integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
+ "dependencies": {
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
+ "integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/reactivity-transform": "3.3.4",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
- "magic-string": "^0.25.7",
+ "magic-string": "^0.30.0",
"postcss": "^8.1.10",
- "source-map": "^0.6.1"
- }
- },
- "@vue/compiler-ssr": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz",
- "integrity": "sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==",
- "requires": {
- "@vue/compiler-dom": "3.2.47",
- "@vue/shared": "3.2.47"
- }
- },
- "@vue/reactivity": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.47.tgz",
- "integrity": "sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==",
- "requires": {
- "@vue/shared": "3.2.47"
- }
- },
- "@vue/reactivity-transform": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz",
- "integrity": "sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==",
- "requires": {
- "@babel/parser": "^7.16.4",
- "@vue/compiler-core": "3.2.47",
- "@vue/shared": "3.2.47",
+ "source-map-js": "^1.0.2"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
+ "integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
+ "dependencies": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.0.tgz",
+ "integrity": "sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q=="
+ },
+ "node_modules/@vue/language-core": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.0.tgz",
+ "integrity": "sha512-rOAtqIRyyZ6OQreAkFDbbDt7L5BwvzrdbWaBAoEZjr4ImPBV9cRDBHxlMBU0SBOAZxIUQdjOvQ0uAl9uZDer0w==",
+ "dev": true,
+ "dependencies": {
+ "@volar/language-core": "1.7.6",
+ "@volar/source-map": "1.7.6",
+ "@vue/compiler-dom": "^3.3.0",
+ "@vue/reactivity": "^3.3.0",
+ "@vue/shared": "^3.3.0",
+ "minimatch": "^9.0.0",
+ "muggle-string": "^0.3.1",
+ "vue-template-compiler": "^2.7.14"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz",
+ "integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
+ "dependencies": {
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/@vue/reactivity-transform": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
+ "integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@vue/compiler-core": "3.3.4",
+ "@vue/shared": "3.3.4",
"estree-walker": "^2.0.2",
- "magic-string": "^0.25.7"
+ "magic-string": "^0.30.0"
}
},
- "@vue/runtime-core": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.47.tgz",
- "integrity": "sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==",
- "requires": {
- "@vue/reactivity": "3.2.47",
- "@vue/shared": "3.2.47"
+ "node_modules/@vue/runtime-core": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
+ "integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
+ "dependencies": {
+ "@vue/reactivity": "3.3.4",
+ "@vue/shared": "3.3.4"
}
},
- "@vue/runtime-dom": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz",
- "integrity": "sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==",
- "requires": {
- "@vue/runtime-core": "3.2.47",
- "@vue/shared": "3.2.47",
- "csstype": "^2.6.8"
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
+ "integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
+ "dependencies": {
+ "@vue/runtime-core": "3.3.4",
+ "@vue/shared": "3.3.4",
+ "csstype": "^3.1.1"
}
},
- "@vue/server-renderer": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.47.tgz",
- "integrity": "sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==",
- "requires": {
- "@vue/compiler-ssr": "3.2.47",
- "@vue/shared": "3.2.47"
+ "node_modules/@vue/server-renderer": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
+ "integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.3.4",
+ "@vue/shared": "3.3.4"
+ },
+ "peerDependencies": {
+ "vue": "3.3.4"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz",
+ "integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
+ },
+ "node_modules/@vue/typescript": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@vue/typescript/-/typescript-1.8.0.tgz",
+ "integrity": "sha512-swi0NM+dpZCldXkMGS8wCxvoiRgA0PJw0UQeSTA7PqB2/5LsOQ8pmxyqLPE6YsbEdn0XqI9a7QgKOmmElkaMOA==",
+ "dev": true,
+ "dependencies": {
+ "@volar/typescript": "1.7.6",
+ "@vue/language-core": "1.8.0"
}
},
- "@vue/shared": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.47.tgz",
- "integrity": "sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ=="
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/axios": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
+ "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
},
- "balanced-match": {
+ "node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
- "brace-expansion": {
+ "node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
- "requires": {
+ "dependencies": {
"balanced-match": "^1.0.0"
}
},
- "csstype": {
- "version": "2.6.21",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
- "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
+ "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
+ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
},
- "de-indent": {
+ "node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
- "esbuild": {
- "version": "0.17.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.12.tgz",
- "integrity": "sha512-bX/zHl7Gn2CpQwcMtRogTTBf9l1nl+H6R8nUbjk+RuKqAE3+8FDulLA+pHvX7aA7Xe07Iwa+CWvy9I8Y2qqPKQ==",
- "dev": true,
- "requires": {
- "@esbuild/android-arm": "0.17.12",
- "@esbuild/android-arm64": "0.17.12",
- "@esbuild/android-x64": "0.17.12",
- "@esbuild/darwin-arm64": "0.17.12",
- "@esbuild/darwin-x64": "0.17.12",
- "@esbuild/freebsd-arm64": "0.17.12",
- "@esbuild/freebsd-x64": "0.17.12",
- "@esbuild/linux-arm": "0.17.12",
- "@esbuild/linux-arm64": "0.17.12",
- "@esbuild/linux-ia32": "0.17.12",
- "@esbuild/linux-loong64": "0.17.12",
- "@esbuild/linux-mips64el": "0.17.12",
- "@esbuild/linux-ppc64": "0.17.12",
- "@esbuild/linux-riscv64": "0.17.12",
- "@esbuild/linux-s390x": "0.17.12",
- "@esbuild/linux-x64": "0.17.12",
- "@esbuild/netbsd-x64": "0.17.12",
- "@esbuild/openbsd-x64": "0.17.12",
- "@esbuild/sunos-x64": "0.17.12",
- "@esbuild/win32-arm64": "0.17.12",
- "@esbuild/win32-ia32": "0.17.12",
- "@esbuild/win32-x64": "0.17.12"
- }
- },
- "estree-walker": {
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
- "fsevents": {
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
- "optional": true
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
},
- "function-bind": {
+ "node_modules/function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+ "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "has": {
+ "node_modules/has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
- "requires": {
+ "dependencies": {
"function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
+ "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "he": {
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true
+ "dev": true,
+ "bin": {
+ "he": "bin/he"
+ }
},
- "is-core-module": {
- "version": "2.11.0",
- "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz",
- "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==",
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
- "requires": {
- "has": "^1.0.3"
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz",
+ "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.13"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
}
},
- "magic-string": {
- "version": "0.25.9",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
- "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
- "requires": {
- "sourcemap-codec": "^1.4.8"
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
}
},
- "minimatch": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-6.2.0.tgz",
- "integrity": "sha512-sauLxniAmvnhhRjFwPNnJKaPFYyddAgbYdeUpHULtCT/GhzdCx/MDNy+Y40lBxTQUrMzDE8e0S43Z5uqfO0REg==",
+ "node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
"dev": true,
- "requires": {
+ "dependencies": {
"brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
- "muggle-string": {
- "version": "0.2.2",
- "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz",
- "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==",
+ "node_modules/muggle-string": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
+ "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
"dev": true
},
- "nanoid": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz",
- "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw=="
+ "node_modules/nanoid": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz",
+ "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
},
- "path-parse": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
- "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
- "dev": true
+ "node_modules/object-inspect": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
+ "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "picocolors": {
+ "node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
},
- "postcss": {
- "version": "8.4.21",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz",
- "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==",
- "requires": {
- "nanoid": "^3.3.4",
+ "node_modules/postcss": {
+ "version": "8.4.24",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz",
+ "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
}
},
- "resolve": {
- "version": "1.22.1",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
- "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==",
- "dev": true,
- "requires": {
- "is-core-module": "^2.9.0",
- "path-parse": "^1.0.7",
- "supports-preserve-symlinks-flag": "^1.0.0"
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "node_modules/qs": {
+ "version": "6.11.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz",
+ "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "rollup": {
- "version": "3.19.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.19.1.tgz",
- "integrity": "sha512-lAbrdN7neYCg/8WaoWn/ckzCtz+jr70GFfYdlf50OF7387HTg+wiuiqJRFYawwSPpqfqDNYqK7smY/ks2iAudg==",
+ "node_modules/rollup": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz",
+ "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==",
"dev": true,
- "requires": {
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=14.18.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
"fsevents": "~2.3.2"
}
},
- "source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ "node_modules/semver": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz",
+ "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
+ "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
- "source-map-js": {
+ "node_modules/source-map-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
- "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw=="
- },
- "sourcemap-codec": {
- "version": "1.4.8",
- "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
- "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
- },
- "supports-preserve-symlinks-flag": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
- "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
- "dev": true
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
- "typescript": {
+ "node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
- "dev": true
+ "dev": true,
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
},
- "vite": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-4.2.0.tgz",
- "integrity": "sha512-AbDTyzzwuKoRtMIRLGNxhLRuv1FpRgdIw+1y6AQG73Q5+vtecmvzKo/yk8X/vrHDpETRTx01ABijqUHIzBXi0g==",
+ "node_modules/uuid": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
+ "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vite": {
+ "version": "4.3.9",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz",
+ "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==",
"dev": true,
- "requires": {
+ "dependencies": {
"esbuild": "^0.17.5",
- "fsevents": "~2.3.2",
- "postcss": "^8.4.21",
- "resolve": "^1.22.1",
- "rollup": "^3.18.0"
+ "postcss": "^8.4.23",
+ "rollup": "^3.21.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 14",
+ "less": "*",
+ "sass": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
}
},
- "vue": {
- "version": "3.2.47",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.2.47.tgz",
- "integrity": "sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==",
- "requires": {
- "@vue/compiler-dom": "3.2.47",
- "@vue/compiler-sfc": "3.2.47",
- "@vue/runtime-dom": "3.2.47",
- "@vue/server-renderer": "3.2.47",
- "@vue/shared": "3.2.47"
+ "node_modules/vscode-uri": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz",
+ "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==",
+ "dev": true
+ },
+ "node_modules/vue": {
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz",
+ "integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
+ "dependencies": {
+ "@vue/compiler-dom": "3.3.4",
+ "@vue/compiler-sfc": "3.3.4",
+ "@vue/runtime-dom": "3.3.4",
+ "@vue/server-renderer": "3.3.4",
+ "@vue/shared": "3.3.4"
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.2.2.tgz",
+ "integrity": "sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==",
+ "dependencies": {
+ "@vue/devtools-api": "^6.5.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
}
},
- "vue-template-compiler": {
+ "node_modules/vue-template-compiler": {
"version": "2.7.14",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz",
"integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==",
"dev": true,
- "requires": {
+ "dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
- "vue-tsc": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.2.0.tgz",
- "integrity": "sha512-rIlzqdrhyPYyLG9zxsVRa+JEseeS9s8F2BbVVVWRRsTZvJO2BbhLEb2HW3MY+DFma0378tnIqs+vfTzbcQtRFw==",
+ "node_modules/vue-tsc": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.0.tgz",
+ "integrity": "sha512-zRjRghohec71o+o3dzzqwFLtbKmJ1K1xRnq9ToHRdnHbBSZA2eUaTT1o+y4xOkBLZtW4cv7FkZE0FGCZfMrcBw==",
"dev": true,
- "requires": {
- "@volar/vue-language-core": "1.2.0",
- "@volar/vue-typescript": "1.2.0"
+ "dependencies": {
+ "@vue/language-core": "1.8.0",
+ "@vue/typescript": "1.8.0",
+ "semver": "^7.3.8",
+ "vscode-uri": "^3.0.7"
+ },
+ "bin": {
+ "vue-tsc": "bin/vue-tsc.js"
+ },
+ "peerDependencies": {
+ "typescript": "*"
}
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+ "dev": true
}
}
}
diff --git a/front/package.json b/front/package.json
index a04df5a..cce4a33 100644
--- a/front/package.json
+++ b/front/package.json
@@ -4,17 +4,23 @@
"version": "0.0.0",
"type": "module",
"scripts": {
- "dev": "vite",
+ "dev": "vite --debug --host",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
- "vue": "^3.2.47"
+ "axios": "^1.3.4",
+ "qs": "^6.11.1",
+ "uuid": "^9.0.0",
+ "vue": "^3.2.47",
+ "vue-router": "^4.1.6"
},
"devDependencies": {
+ "@types/qs": "^6.9.7",
+ "@types/uuid": "^9.0.1",
"@vitejs/plugin-vue": "^4.1.0",
"typescript": "^4.9.3",
- "vite": "^4.2.0",
+ "vite": "^4.2.3",
"vue-tsc": "^1.2.0"
}
}
diff --git a/front/public/1F3B4.svg b/front/public/1F3B4.svg
new file mode 100644
index 0000000..35d60eb
--- /dev/null
+++ b/front/public/1F3B4.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/public/The pattern on the back of a playing card, black and white and classic.jpeg b/front/public/The pattern on the back of a playing card, black and white and classic.jpeg
new file mode 100644
index 0000000..a17f36e
Binary files /dev/null and b/front/public/The pattern on the back of a playing card, black and white and classic.jpeg differ
diff --git a/front/src/App.vue b/front/src/App.vue
index bb666a8..6e994e4 100644
--- a/front/src/App.vue
+++ b/front/src/App.vue
@@ -1,30 +1,7 @@
-
-
+
-
-
diff --git a/front/src/axios.ts b/front/src/axios.ts
new file mode 100644
index 0000000..3f0003d
--- /dev/null
+++ b/front/src/axios.ts
@@ -0,0 +1,13 @@
+import axios from 'axios'
+import qs from 'qs'
+
+const instance = axios.create({
+ baseURL: '/api',
+ paramsSerializer: {
+ serialize: (params) => {
+ return qs.stringify(params, { arrayFormat: 'repeat' })
+ },
+ },
+})
+
+export default instance
diff --git a/front/src/components/BetButton.vue b/front/src/components/BetButton.vue
new file mode 100644
index 0000000..37fb4dd
--- /dev/null
+++ b/front/src/components/BetButton.vue
@@ -0,0 +1,41 @@
+
+
+
+ 賭ける
+
+
+
\ No newline at end of file
diff --git a/front/src/components/BetComponent.vue b/front/src/components/BetComponent.vue
new file mode 100644
index 0000000..0f9e116
--- /dev/null
+++ b/front/src/components/BetComponent.vue
@@ -0,0 +1,40 @@
+
+
+
+
+ {{ bet.betAmount }}
+
+ 勝ち
+ 負け
+ 引き分け
+
+
+
+
+
diff --git a/front/src/components/BetDialog.vue b/front/src/components/BetDialog.vue
new file mode 100644
index 0000000..18bb35a
--- /dev/null
+++ b/front/src/components/BetDialog.vue
@@ -0,0 +1,158 @@
+
+
+
+
+
+
賭ける
+
+
+
+ 100
+
+
+
+
+ 500
+
+
+
+
+ 1000
+
+
+
+
+
+
+
+
+
diff --git a/front/src/components/CardComponent.vue b/front/src/components/CardComponent.vue
new file mode 100644
index 0000000..dc8ea91
--- /dev/null
+++ b/front/src/components/CardComponent.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+ {{ PrintableSuit.get(card.suit) }}
+
+
+
+
+ {{ PrintableRank.get(card.rank) }}
+
+
+
+
+
+
diff --git a/front/src/components/HelloWorld.vue b/front/src/components/HelloWorld.vue
deleted file mode 100644
index 7b25f3f..0000000
--- a/front/src/components/HelloWorld.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
- {{ msg }}
-
-
-
count is {{ count }}
-
- Edit
- components/HelloWorld.vue
to test HMR
-
-
-
-
- Check out
- create-vue , the official Vue + Vite starter
-
-
- Install
- Volar
- in your IDE for a better DX
-
- Click on the Vite and Vue logos to learn more
-
-
-
diff --git a/front/src/components/PieComponent.vue b/front/src/components/PieComponent.vue
new file mode 100644
index 0000000..51a8f47
--- /dev/null
+++ b/front/src/components/PieComponent.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
diff --git a/front/src/components/ShuffleAnimation.vue b/front/src/components/ShuffleAnimation.vue
new file mode 100644
index 0000000..ea404e7
--- /dev/null
+++ b/front/src/components/ShuffleAnimation.vue
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
diff --git a/front/src/computedStyles/namedColorSet.ts b/front/src/computedStyles/namedColorSet.ts
new file mode 100644
index 0000000..c2277f6
--- /dev/null
+++ b/front/src/computedStyles/namedColorSet.ts
@@ -0,0 +1,18 @@
+const nameToColorMap = new Map([
+ ["サクラ", 'material-red'],
+ ["ウメ", 'material-pink'],
+ ["カエデ", 'material-purple'],
+ ["シラカバ", 'material-deep-purple'],
+ ["アジサイ", 'material-indigo'],
+ ["ツツジ", 'material-blue'],
+ ["モミジ", 'material-light-blue'],
+ ["ツバキ", 'material-cyan'],
+ ["マツ", 'material-teal'],
+ ["モクレン", 'material-green'],
+ ["タケ", 'material-light-green'],
+ ["ヒイラギ", 'material-lime'],
+]) as ReadonlyMap
+
+export const namedColorSet = (name: string): string => {
+ return nameToColorMap.get(name) || 'material-grey'
+}
diff --git a/front/src/main.ts b/front/src/main.ts
index 2425c0f..3273150 100644
--- a/front/src/main.ts
+++ b/front/src/main.ts
@@ -1,5 +1,8 @@
import { createApp } from 'vue'
import './style.css'
+import router from './router'
import App from './App.vue'
-createApp(App).mount('#app')
+const app = createApp(App)
+app.use(router)
+app.mount('#app')
diff --git a/front/src/router.ts b/front/src/router.ts
new file mode 100644
index 0000000..edc5341
--- /dev/null
+++ b/front/src/router.ts
@@ -0,0 +1,27 @@
+import qs from 'qs'
+import { createRouter, createWebHistory } from 'vue-router'
+import HomeView from './views/HomeView.vue'
+import RoomView from './views/RoomView.vue'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: HomeView
+ },
+ {
+ path: '/room',
+ name: 'room',
+ component: RoomView
+ },
+ {
+ path: '/*',
+ redirect: '/'
+ }
+ ],
+ stringifyQuery: (query) => { return qs.stringify(query, { arrayFormat: 'repeat' }) }
+})
+
+export default router
diff --git a/front/src/services/RoomService.ts b/front/src/services/RoomService.ts
new file mode 100644
index 0000000..3901dd0
--- /dev/null
+++ b/front/src/services/RoomService.ts
@@ -0,0 +1,59 @@
+import { AxiosError } from 'axios'
+import axios from '../axios'
+
+import { Room } from '../types/Room'
+
+export default class RoomService {
+ static async enterRoom(roomId: string): Promise {
+ const { data } = await axios.post(`/room/`, { id: roomId })
+ if (data.status == 503) {
+ console.log('Room is full')
+ }
+ return data
+ }
+
+ static isSubscribed = false
+
+ static stopSubscribe() {
+ this.isSubscribed = false
+ }
+
+ static async subscribeToRoom(roomId: string, callback: (room: Room) => void): Promise {
+ if (this.isSubscribed) {
+ return Promise.reject('Already subscribed')
+ }
+ this.isSubscribed = true
+ while (this.isSubscribed) {
+ try {
+ const response = await axios.post(`/room/subscribe`, { id: roomId })
+ if (!this.isSubscribed) {
+ return Promise.reject('Unsubscribed')
+ }
+ if (response.status == 200) {
+ let room = response.data
+ callback(room)
+ } else {
+ console.log("not 200: ", response.status, response.data.message)
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ console.log("Error: ", error.response?.data.message)
+ }
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ }
+ await new Promise((resolve) => setTimeout(resolve, 500))
+ }
+ return Promise.reject('Unsubscribed')
+ }
+
+ static async bet(roomId: string, userName: string, handIndex: number, betAmount: number): Promise {
+ const { data } = await axios.post(`/room/bet`, { roomId, userName, handIndex, betAmount })
+ return data
+ }
+
+ static async requestCard(roomId: string, userName: string, handIndex: number): Promise {
+ const { data } = await axios.post(`/room/requestCard`, { roomId, userName, handIndex })
+ return data
+ }
+}
diff --git a/front/src/style.css b/front/src/style.css
index 7294765..fa09fc5 100644
--- a/front/src/style.css
+++ b/front/src/style.css
@@ -1,80 +1,69 @@
-:root {
- font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
+/* Material Colors with Contrasting Text Colors */
+.material-grey {
+ color: #ffffff;
+ background-color: #9e9e9e;
+}
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- -webkit-text-size-adjust: 100%;
+.material-red {
+ color: #ffffff;
+ background-color: #f44336;
}
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
+.material-pink {
+ color: #ffffff;
+ background-color: #e91e63;
}
-a:hover {
- color: #535bf2;
+
+.material-purple {
+ color: #ffffff;
+ background-color: #9c27b0;
}
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
+.material-deep-purple {
+ color: #ffffff;
+ background-color: #673ab7;
}
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
+.material-indigo {
+ color: #ffffff;
+ background-color: #3f51b5;
}
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
+.material-blue {
+ color: #ffffff;
+ background-color: #2196f3;
}
-button:hover {
- border-color: #646cff;
+
+.material-light-blue {
+ color: #000000;
+ background-color: #03a9f4;
+}
+
+.material-cyan {
+ color: #000000;
+ background-color: #00bcd4;
}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
+
+.material-teal {
+ color: #ffffff;
+ background-color: #009688;
}
-.card {
- padding: 2em;
+.material-green {
+ color: #ffffff;
+ background-color: #4caf50;
}
-#app {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
+.material-light-green {
+ color: #000000;
+ background-color: #8bc34a;
}
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
+.material-lime {
+ color: #000000;
+ background-color: #cddc39;
}
diff --git a/front/src/types/Bet.ts b/front/src/types/Bet.ts
new file mode 100644
index 0000000..7e4a9c9
--- /dev/null
+++ b/front/src/types/Bet.ts
@@ -0,0 +1,7 @@
+export interface Bet {
+ roomId: string
+ userName: string
+ handIndex: number
+ betAmount: number
+ result: string
+}
diff --git a/front/src/types/Card.ts b/front/src/types/Card.ts
new file mode 100644
index 0000000..dc5e83a
--- /dev/null
+++ b/front/src/types/Card.ts
@@ -0,0 +1,54 @@
+export const Suit = {
+ SPADE: 'SPADE',
+ HEART: 'HEART',
+ DIAMOND: 'DIAMOND',
+ CLUB: 'CLUB',
+} as const
+
+export type SuitType = typeof Suit[keyof typeof Suit];
+
+export const PrintableSuit: Map = new Map([
+ [Suit.SPADE, '♠'],
+ [Suit.HEART, '♥'],
+ [Suit.DIAMOND, '♦'],
+ [Suit.CLUB, '♣'],
+])
+
+export const Rank = {
+ ACE: 'ACE',
+ TWO: 'TWO',
+ THREE: 'THREE',
+ FOUR: 'FOUR',
+ FIVE: 'FIVE',
+ SIX: 'SIX',
+ SEVEN: 'SEVEN',
+ EIGHT: 'EIGHT',
+ NINE: 'NINE',
+ TEN: 'TEN',
+ JACK: 'JACK',
+ QUEEN: 'QUEEN',
+ KING: 'KING',
+} as const
+
+export type RankType = typeof Rank[keyof typeof Rank];
+
+export const PrintableRank: Map = new Map([
+ [Rank.ACE, 'A'],
+ [Rank.TWO, '2'],
+ [Rank.THREE, '3'],
+ [Rank.FOUR, '4'],
+ [Rank.FIVE, '5'],
+ [Rank.SIX, '6'],
+ [Rank.SEVEN, '7'],
+ [Rank.EIGHT, '8'],
+ [Rank.NINE, '9'],
+ [Rank.TEN, '10'],
+ [Rank.JACK, 'J'],
+ [Rank.QUEEN, 'Q'],
+ [Rank.KING, 'K'],
+])
+
+export interface Card {
+ suit: SuitType
+ rank: RankType
+}
diff --git a/front/src/types/Room.ts b/front/src/types/Room.ts
new file mode 100644
index 0000000..bdffafa
--- /dev/null
+++ b/front/src/types/Room.ts
@@ -0,0 +1,21 @@
+import { Bet } from './Bet'
+import { Card } from './Card'
+import { StatusType } from './Status'
+
+export interface Room {
+ id: string
+ yourName: string
+ members: string[]
+ wallets: any
+ hands1: Card[]
+ hands2: Card[]
+ hands3: Card[]
+ hands4: Card[]
+ hands5: Card[]
+ hands6: Card[]
+ hands7: Card[]
+ bets: Bet[]
+ status: StatusType
+ timeLeft: number
+ timeLeftDenominator: number
+}
diff --git a/front/src/types/Status.ts b/front/src/types/Status.ts
new file mode 100644
index 0000000..cbcf8ec
--- /dev/null
+++ b/front/src/types/Status.ts
@@ -0,0 +1,12 @@
+export const Status = {
+ START: 'START',
+ SHUFFLE: 'SHUFFLE',
+ HAND_OUT_CARDS: 'HAND_OUT_CARDS',
+ WAIT_TO_BET: 'WAIT_TO_BET',
+ WAIT_TO_REQUEST: 'WAIT_TO_REQUEST',
+ DEALER_TURN: 'DEALER_TURN',
+ LIQUIDATION: 'LIQUIDATION',
+ END: 'END',
+} as const
+
+export type StatusType = typeof Status[keyof typeof Status];
diff --git a/front/src/views/HomeView.vue b/front/src/views/HomeView.vue
new file mode 100644
index 0000000..358f08c
--- /dev/null
+++ b/front/src/views/HomeView.vue
@@ -0,0 +1,78 @@
+
+
+
+ Oi
+
+ ユーザ参加数: {{ count }}
+
+
+
+
+
+
diff --git a/front/src/views/RoomView.vue b/front/src/views/RoomView.vue
new file mode 100644
index 0000000..2a7fa1c
--- /dev/null
+++ b/front/src/views/RoomView.vue
@@ -0,0 +1,388 @@
+
+
+
+ {{ roomId }}
+
+
+ あなたの名前 : {{ room.yourName }}
+
+ {{ walletByUserName(room.yourName) }}
+
+
+
+
+
ステータス:
+
(観戦中)
+
ゲーム開始
+
シャッフル中
+
配布中
+
賭けてください
+
もう一枚引きますか?
+
親の番
+
精算中
+
ゲーム終了
+
+
+
+
+
+
+
+
+ ユーザー名
+ 金額
+
+
+
+
+
+
+ {{ row.userName }}
+
+
+ {{ row.amount }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 親の手札
+
+
+
+
+
+ もう一枚
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/front/tsconfig.node.json b/front/tsconfig.node.json
index 9d31e2a..a6dfb11 100644
--- a/front/tsconfig.node.json
+++ b/front/tsconfig.node.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "strict": true,
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
diff --git a/front/vite.config.ts b/front/vite.config.ts
index b202521..7fcdadb 100644
--- a/front/vite.config.ts
+++ b/front/vite.config.ts
@@ -10,7 +10,7 @@ export default defineConfig({
server: {
proxy: {
'/api': {
- target: 'http://localhost:8080',
+ target: 'http://127.0.0.1:8080',
changeOrigin: true,
}
}
diff --git a/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java b/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java
new file mode 100644
index 0000000..5337fda
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/common/RoomLimitation.java
@@ -0,0 +1,10 @@
+package jp.moreslowly.oi.common;
+
+public final class RoomLimitation {
+ public static final int MAX_ROOM_SIZE = 200;
+ public static final int MAX_MEMBER_SIZE = 6;
+ public static final int MAX_HAND_OUT_SIZE = 7;
+
+ private RoomLimitation() {
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/common/SessionKey.java b/src/main/java/jp/moreslowly/oi/common/SessionKey.java
new file mode 100644
index 0000000..3cb3dd9
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/common/SessionKey.java
@@ -0,0 +1,10 @@
+package jp.moreslowly.oi.common;
+
+public final class SessionKey {
+ public static final String USER_ID = "userId";
+ public static final String NICKNAME = "nickname";
+ public static final String ROOM_ID = "roomId";
+
+ private SessionKey() {
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/RedisConfig.java b/src/main/java/jp/moreslowly/oi/config/RedisConfig.java
new file mode 100644
index 0000000..74e4550
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/RedisConfig.java
@@ -0,0 +1,28 @@
+package jp.moreslowly.oi.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
+import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
+
+@Configuration
+@EnableRedisRepositories
+@EnableRedisHttpSession
+public class RedisConfig {
+ @Bean
+ RedisConnectionFactory connectionFactory() {
+ return new LettuceConnectionFactory();
+ }
+
+ @Bean
+ RedisTemplate, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
+
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ return template;
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java b/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java
new file mode 100644
index 0000000..f2f459d
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/RestResponseEntityExceptionHandler.java
@@ -0,0 +1,14 @@
+package jp.moreslowly.oi.config;
+
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
+
+@ControllerAdvice
+public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
+ @ExceptionHandler(value = { IllegalArgumentException.class })
+ protected org.springframework.http.ResponseEntity handleIllegalArgumentException(
+ RuntimeException ex) {
+ return org.springframework.http.ResponseEntity.badRequest().body(ex.getMessage());
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java b/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java
new file mode 100644
index 0000000..c660aba
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/SchedulingConfig.java
@@ -0,0 +1,23 @@
+package jp.moreslowly.oi.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import jp.moreslowly.oi.tasks.DealerManager;
+import jp.moreslowly.oi.tasks.SweeperTask;
+
+@Configuration
+@EnableScheduling
+public class SchedulingConfig {
+
+ @Bean
+ public DealerManager dealerManager() {
+ return new DealerManager();
+ }
+
+ @Bean
+ public SweeperTask sweeperTask() {
+ return new SweeperTask();
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java b/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java
new file mode 100644
index 0000000..357c2b4
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/ServletCustomizer.java
@@ -0,0 +1,17 @@
+package jp.moreslowly.oi.config;
+
+import org.springframework.boot.web.server.ErrorPage;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Component;
+
+@Component
+public class ServletCustomizer implements WebServerFactoryCustomizer {
+ @Override
+ public void customize(ConfigurableServletWebServerFactory factory) {
+ // vue-routerをHistoryモードで動かすと、/以外が404となってしまうため、index.htmlに飛ばすように修正。
+ ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/index.html");
+ factory.addErrorPages(error404Page);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java b/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java
new file mode 100644
index 0000000..ff9fb3c
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/TomcatConfig.java
@@ -0,0 +1,43 @@
+package jp.moreslowly.oi.config;
+
+import java.util.Optional;
+
+import org.apache.catalina.connector.Connector;
+import org.apache.coyote.ajp.AbstractAjpProtocol;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
+import org.springframework.boot.web.server.WebServerFactoryCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import ch.qos.logback.access.tomcat.LogbackValve;
+
+@Configuration
+public class TomcatConfig {
+ @Value("${ajp.secret}")
+ private String ajpSecret;
+
+ @Bean
+ public WebServerFactoryCustomizer servletContainer() {
+ return server -> {
+ LogbackValve logbackValve = new LogbackValve();
+ logbackValve.setAsyncSupported(true);
+ Optional.ofNullable(server)
+ .ifPresent(s -> s.addContextValves(logbackValve));
+ Optional.ofNullable(server)
+ .ifPresent(s -> s.addAdditionalTomcatConnectors(redirectConnector()));
+ };
+ }
+
+ private Connector redirectConnector() {
+ Connector connector = new Connector("AJP/1.3");
+ connector.setScheme("http");
+ connector.setPort(8009);
+ connector.setAllowTrace(false);
+
+ final AbstractAjpProtocol protocol = (AbstractAjpProtocol) connector.getProtocolHandler();
+ connector.setSecure(true);
+ protocol.setSecret(ajpSecret);
+ return connector;
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java b/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java
new file mode 100644
index 0000000..336fa94
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/config/WebMvcConfig.java
@@ -0,0 +1,30 @@
+package jp.moreslowly.oi.config;
+
+import java.util.concurrent.TimeUnit;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.CacheControl;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class WebMvcConfig implements WebMvcConfigurer {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ // viteとの開発用に、3000からのアクセスを許可する。
+ registry.addMapping("/api/**")
+ .allowedMethods("*")
+ .allowedOrigins("http://localhost:3000",
+ "http://127.0.0.1:5173",
+ "http://localhost:5173",
+ "http://192.168.1.175:5173");
+ }
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry) {
+ registry.addResourceHandler("/index.html")
+ .addResourceLocations("classpath:/static/")
+ .setCacheControl(CacheControl.maxAge(0, TimeUnit.SECONDS).cachePrivate());
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/controllers/RoomController.java b/src/main/java/jp/moreslowly/oi/controllers/RoomController.java
new file mode 100644
index 0000000..c681789
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/controllers/RoomController.java
@@ -0,0 +1,102 @@
+package jp.moreslowly.oi.controllers;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import jakarta.servlet.http.HttpSession;
+import jp.moreslowly.oi.common.SessionKey;
+import jp.moreslowly.oi.dto.BetDto;
+import jp.moreslowly.oi.dto.IdDto;
+import jp.moreslowly.oi.dto.RequestCardDto;
+import jp.moreslowly.oi.dto.RoomDto;
+import jp.moreslowly.oi.exception.UnprocessableContentException;
+import jp.moreslowly.oi.service.RoomService;
+
+@RestController
+@RequestMapping("/api/room")
+public class RoomController {
+
+ @Autowired
+ private RoomService roomService;
+
+ @Autowired
+ private HttpSession session;
+
+ @GetMapping("/")
+ public List getRoomIdList() {
+ return roomService.getRoomIdList();
+ }
+
+ @PostMapping("/")
+ public RoomDto enterRoom(@RequestBody IdDto dto) {
+ // UUID validation
+ UUID.fromString(dto.getId());
+
+ return roomService.enterRoom(session, dto.getId());
+ }
+
+ @PostMapping("/subscribe")
+ public DeferredResult subscribe(@RequestBody IdDto dto) {
+ // UUID validation
+ UUID.fromString(dto.getId());
+
+ DeferredResult deferredResult = new DeferredResult<>();
+ roomService.subscribe(dto.getId(), session, deferredResult);
+
+ return deferredResult;
+ }
+
+ @PostMapping("/reset")
+ public void reset(@RequestBody IdDto dto) {
+ // UUID validation
+ UUID.fromString(dto.getId());
+
+ roomService.reset(dto.getId());
+ }
+
+ @PostMapping("/bet")
+ public void bet(@RequestBody BetDto dto) {
+ // UUID validation
+ UUID roomIdUUID = UUID.fromString(dto.getRoomId());
+
+ String sessionRoomId = (String) session.getAttribute(SessionKey.ROOM_ID);
+ UUID sessionRoomIdUUID = UUID.fromString(sessionRoomId);
+ if (!roomIdUUID.equals(sessionRoomIdUUID)) {
+ throw new UnprocessableContentException("Invalid room id");
+ }
+
+ String sessionNickname = (String) session.getAttribute(SessionKey.NICKNAME);
+ if (!dto.getUserName().equals(sessionNickname)) {
+ throw new UnprocessableContentException("Invalid nickname on bet validation");
+ }
+
+ roomService.bet(session, dto);
+ }
+
+ @PostMapping("/requestCard")
+ public void requestOneMore(@RequestBody RequestCardDto dto) {
+ // UUID validation
+ UUID roomIdUUID = UUID.fromString(dto.getRoomId());
+
+ String sessionRoomId = (String) session.getAttribute(SessionKey.ROOM_ID);
+ UUID sessionRoomIdUUID = UUID.fromString(sessionRoomId);
+ if (!roomIdUUID.equals(sessionRoomIdUUID)) {
+ throw new UnprocessableContentException("Invalid room id");
+ }
+
+ String sessionNickname = (String) session.getAttribute(SessionKey.NICKNAME);
+ if (!dto.getUserName().equals(sessionNickname)) {
+ throw new UnprocessableContentException("Invalid nickname on request card validation");
+ }
+
+ roomService.requestCard(session, dto);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/controllers/UsersController.java b/src/main/java/jp/moreslowly/oi/controllers/UsersController.java
new file mode 100644
index 0000000..02b7f42
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/controllers/UsersController.java
@@ -0,0 +1,14 @@
+package jp.moreslowly.oi.controllers;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/users")
+public class UsersController {
+ @GetMapping("/count")
+ public String count() {
+ return "たくさん";
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/dao/Bet.java b/src/main/java/jp/moreslowly/oi/dao/Bet.java
new file mode 100644
index 0000000..4258f87
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dao/Bet.java
@@ -0,0 +1,35 @@
+package jp.moreslowly.oi.dao;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@RedisHash("bet")
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class Bet {
+ public enum Result {
+ WIN("WIN"),
+ LOSE("LOSE"),
+ DRAW("DRAW");
+ private String value;
+ private Result(String value) {
+ this.value = value;
+ }
+ public String getValue() {
+ return value;
+ }
+ }
+ @Id private String id;
+ private String roomId;
+ private String userName;
+ private Integer handIndex;
+ private Integer betAmount;
+ private Result result;
+}
diff --git a/src/main/java/jp/moreslowly/oi/dao/Room.java b/src/main/java/jp/moreslowly/oi/dao/Room.java
new file mode 100644
index 0000000..84f48df
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dao/Room.java
@@ -0,0 +1,156 @@
+package jp.moreslowly.oi.dao;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.redis.core.RedisHash;
+
+import jp.moreslowly.oi.models.Card;
+import jp.moreslowly.oi.models.Member;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+
+@RedisHash("room")
+@Data
+@SuperBuilder
+@AllArgsConstructor
+public class Room {
+ public enum Status {
+ START,
+ SHUFFLE,
+ HAND_OUT_CARDS,
+ WAIT_TO_BET,
+ WAIT_TO_REQUEST,
+ DEALER_TURN,
+ LIQUIDATION,
+ END;
+ private static final Status[] values = values();
+ public Status prev() {
+ return values[(this.ordinal() - 1 + values.length) % values.length];
+ }
+ public Status next() {
+ return values[(this.ordinal() + 1) % values.length];
+ }
+ }
+
+ @Id private String id;
+ @Builder.Default private List members = new ArrayList<>();
+ @Builder.Default private Map wallets = new HashMap<>();
+ @Builder.Default private List deck = new ArrayList<>();
+ @Builder.Default private List hands1 = new ArrayList<>();
+ @Builder.Default private List hands2 = new ArrayList<>();
+ @Builder.Default private List hands3 = new ArrayList<>();
+ @Builder.Default private List hands4 = new ArrayList<>();
+ @Builder.Default private List hands5 = new ArrayList<>();
+ @Builder.Default private List hands6 = new ArrayList<>();
+ @Builder.Default private List hands7 = new ArrayList<>();
+ @Builder.Default private List bets = new ArrayList<>();
+ @Builder.Default private Status status = Status.START;
+ @Builder.Default private Long timeLeft = 0L;
+ @Builder.Default private Long timeLeftDenominator = 1L;
+ private LocalDateTime lastAccessedAt;
+ private LocalDateTime updatedAt;
+
+ public Room() {
+ this.status = Status.START;
+ }
+
+ public void reset() {
+ this.deck = new ArrayList<>();
+ if (Objects.isNull(this.members)) {
+ this.members = new ArrayList<>();
+ }
+ this.hands1 = new ArrayList<>();
+ this.hands2 = new ArrayList<>();
+ this.hands3 = new ArrayList<>();
+ this.hands4 = new ArrayList<>();
+ this.hands5 = new ArrayList<>();
+ this.hands6 = new ArrayList<>();
+ this.hands7 = new ArrayList<>();
+ this.bets = new ArrayList<>();
+ this.status = Status.START;
+ this.timeLeft = 0L;
+ this.timeLeftDenominator = 1L;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public List getHandsAt(int index) {
+ switch (index) {
+ case 1:
+ if (Objects.isNull(this.hands1)) {
+ this.hands1 = new ArrayList<>();
+ }
+ return getHands1();
+ case 2:
+ if (Objects.isNull(this.hands2)) {
+ this.hands2 = new ArrayList<>();
+ }
+ return getHands2();
+ case 3:
+ if (Objects.isNull(this.hands3)) {
+ this.hands3 = new ArrayList<>();
+ }
+ return getHands3();
+ case 4:
+ if (Objects.isNull(this.hands4)) {
+ this.hands4 = new ArrayList<>();
+ }
+ return getHands4();
+ case 5:
+ if (Objects.isNull(this.hands5)) {
+ this.hands5 = new ArrayList<>();
+ }
+ return getHands5();
+ case 6:
+ if (Objects.isNull(this.hands6)) {
+ this.hands6 = new ArrayList<>();
+ }
+ return getHands6();
+ case 7:
+ if (Objects.isNull(this.hands7)) {
+ this.hands7 = new ArrayList<>();
+ }
+ return getHands7();
+ default:
+ throw new IllegalArgumentException("index must be 1 to 7");
+ }
+ }
+
+ public void setHandsAt(int index, List hands) {
+ if (Objects.isNull(hands)) {
+ throw new IllegalArgumentException("hands must not be null");
+ }
+ switch (index) {
+ case 1:
+ setHands1(hands);
+ break;
+ case 2:
+ setHands2(hands);
+ break;
+ case 3:
+ setHands3(hands);
+ break;
+ case 4:
+ setHands4(hands);
+ break;
+ case 5:
+ setHands5(hands);
+ break;
+ case 6:
+ setHands6(hands);
+ break;
+ case 7:
+ setHands7(hands);
+ break;
+ default:
+ throw new IllegalArgumentException("index must be 1 to 7");
+ }
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/dto/BetDto.java b/src/main/java/jp/moreslowly/oi/dto/BetDto.java
new file mode 100644
index 0000000..47c4cf0
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dto/BetDto.java
@@ -0,0 +1,39 @@
+package jp.moreslowly.oi.dto;
+
+import jp.moreslowly.oi.dao.Bet;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class BetDto {
+ private String roomId;
+ private String userName;
+ private Integer handIndex;
+ private Integer betAmount;
+ private Bet.Result result;
+
+ public static BetDto fromEntity(Bet bet) {
+ return BetDto.builder()
+ .roomId(bet.getRoomId())
+ .userName(bet.getUserName())
+ .handIndex(bet.getHandIndex())
+ .betAmount(bet.getBetAmount())
+ .result(bet.getResult())
+ .build();
+ }
+
+ public Bet toEntity() {
+ return Bet.builder()
+ .roomId(this.roomId)
+ .userName(this.userName)
+ .handIndex(this.handIndex)
+ .betAmount(this.betAmount)
+ .result(this.result)
+ .build();
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/dto/IdDto.java b/src/main/java/jp/moreslowly/oi/dto/IdDto.java
new file mode 100644
index 0000000..026d61e
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dto/IdDto.java
@@ -0,0 +1,14 @@
+package jp.moreslowly.oi.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class IdDto {
+ private String id;
+}
diff --git a/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java b/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java
new file mode 100644
index 0000000..8e62376
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dto/RequestCardDto.java
@@ -0,0 +1,16 @@
+package jp.moreslowly.oi.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class RequestCardDto {
+ private String roomId;
+ private String userName;
+ private Integer handIndex;
+}
diff --git a/src/main/java/jp/moreslowly/oi/dto/RoomDto.java b/src/main/java/jp/moreslowly/oi/dto/RoomDto.java
new file mode 100644
index 0000000..e2e5a95
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/dto/RoomDto.java
@@ -0,0 +1,179 @@
+package jp.moreslowly.oi.dto;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import jp.moreslowly.oi.dao.Bet;
+import jp.moreslowly.oi.dao.Room;
+import jp.moreslowly.oi.dao.Room.Status;
+import jp.moreslowly.oi.models.Card;
+import jp.moreslowly.oi.models.Member;
+import lombok.Data;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+public class RoomDto {
+ private String id;
+ private String yourName;
+ private List members;
+ private Map wallets;
+ private List hands1;
+ private List hands2;
+ private List hands3;
+ private List hands4;
+ private List hands5;
+ private List hands6;
+ private List hands7;
+ private List bets;
+ private Long timeLeft;
+ private Long timeLeftDenominator;
+ private Status status;
+
+ public static RoomDto fromEntity(Room room, String yourName) {
+ if (Objects.isNull(room.getMembers())) {
+ room.setMembers(new ArrayList<>());
+ }
+ if (room.getStatus() == Status.START) {
+ return maskedFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.SHUFFLE) {
+ return maskedFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.HAND_OUT_CARDS) {
+ return maskedFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.WAIT_TO_BET) {
+ return maskedWithoutMeFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.WAIT_TO_REQUEST) {
+ return maskedWithoutMeFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.DEALER_TURN) {
+ return maskedWithoutMeFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.LIQUIDATION) {
+ return fullOpenFromEntity(room, yourName);
+ } else if (room.getStatus() == Status.END) {
+ return fullOpenFromEntity(room, yourName);
+ } else {
+ return fullOpenFromEntity(room, yourName);
+ }
+ }
+
+ private static RoomDto fullOpenFromEntity(Room room, String yourName) {
+ return RoomDto.builder()
+ .id(room.getId())
+ .yourName(yourName)
+ .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList()))
+ .wallets(room.getWallets())
+ .hands1(room.getHands1())
+ .hands2(room.getHands2())
+ .hands3(room.getHands3())
+ .hands4(room.getHands4())
+ .hands5(room.getHands5())
+ .hands6(room.getHands6())
+ .hands7(room.getHands7())
+ .bets(Objects.isNull(room.getBets())
+ ? new ArrayList<>()
+ : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList()))
+ .status(room.getStatus())
+ .timeLeft(room.getTimeLeft())
+ .timeLeftDenominator(room.getTimeLeftDenominator())
+ .build();
+ }
+
+ private static RoomDto maskedWithoutMeFromEntity(Room room, String yourName) {
+ List bets = room.getBets();
+ if (Objects.nonNull(bets)) {
+ bets = bets.stream().filter(bet -> bet.getUserName().equals(yourName)).collect(Collectors.toList());
+ } else {
+ bets = new ArrayList<>();
+ }
+ List betIndexes = bets.stream().map(Bet::getHandIndex).collect(Collectors.toList());
+
+ RoomDto dto = RoomDto.builder()
+ .id(room.getId())
+ .yourName(yourName)
+ .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList()))
+ .wallets(room.getWallets())
+ .bets(Objects.isNull(room.getBets())
+ ? new ArrayList<>()
+ : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList()))
+ .status(room.getStatus())
+ .timeLeft(room.getTimeLeft())
+ .timeLeftDenominator(room.getTimeLeftDenominator())
+ .build();
+
+ for (int handIndex : Arrays.asList(1, 2, 3, 4, 5, 6, 7)) {
+ if (betIndexes.contains(handIndex)) {
+ dto.setHandsAt(handIndex, room.getHandsAt(handIndex));
+ } else {
+ List cards = new ArrayList<>();
+ if (room.getHandsAt(handIndex).size() == 3) {
+ cards.add(room.getHandsAt(handIndex).get(0));
+ cards.add(room.getHandsAt(handIndex).get(1));
+ } else {
+ cards.add(room.getHandsAt(handIndex).get(0));
+ }
+ dto.setHandsAt(handIndex, cards);
+ }
+ }
+
+ return dto;
+ }
+
+ private static RoomDto maskedFromEntity(Room room, String yourName) {
+ RoomDto dto = RoomDto.builder()
+ .id(room.getId())
+ .yourName(yourName)
+ .members(room.getMembers().stream().map(Member::getNickname).collect(Collectors.toList()))
+ .wallets(room.getWallets())
+ .bets(Objects.isNull(room.getBets())
+ ? new ArrayList<>()
+ : room.getBets().stream().map(BetDto::fromEntity).collect(Collectors.toList()))
+ .status(room.getStatus())
+ .timeLeft(room.getTimeLeft())
+ .timeLeftDenominator(room.getTimeLeftDenominator())
+ .build();
+
+ for (int handIndex : Arrays.asList(1, 2, 3, 4, 5, 6, 7)) {
+ List cards = new ArrayList<>();
+ if (room.getHandsAt(handIndex).size() == 3) {
+ cards.add(room.getHandsAt(handIndex).get(0));
+ cards.add(room.getHandsAt(handIndex).get(1));
+ } else if (room.getHandsAt(handIndex).size() == 2) {
+ cards.add(room.getHandsAt(handIndex).get(0));
+ }
+ dto.setHandsAt(handIndex, cards);
+ }
+
+ return dto;
+ }
+
+ public void setHandsAt(int handIndex, List cards) {
+ switch (handIndex) {
+ case 1:
+ this.hands1 = cards;
+ break;
+ case 2:
+ this.hands2 = cards;
+ break;
+ case 3:
+ this.hands3 = cards;
+ break;
+ case 4:
+ this.hands4 = cards;
+ break;
+ case 5:
+ this.hands5 = cards;
+ break;
+ case 6:
+ this.hands6 = cards;
+ break;
+ case 7:
+ this.hands7 = cards;
+ break;
+ default:
+ throw new IllegalArgumentException("handIndex must be 1 to 7");
+ }
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java b/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java
new file mode 100644
index 0000000..b52a3bd
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/exception/FullMemberException.java
@@ -0,0 +1,11 @@
+package jp.moreslowly.oi.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+public class FullMemberException extends RuntimeException {
+ public FullMemberException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java b/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java
new file mode 100644
index 0000000..a1f2be2
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/exception/InternalErrorException.java
@@ -0,0 +1,11 @@
+package jp.moreslowly.oi.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
+public class InternalErrorException extends RuntimeException {
+ public InternalErrorException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java b/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java
new file mode 100644
index 0000000..fc3c6cd
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/exception/NoRoomException.java
@@ -0,0 +1,11 @@
+package jp.moreslowly.oi.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
+public class NoRoomException extends RuntimeException {
+ public NoRoomException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java b/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java
new file mode 100644
index 0000000..f68a0c9
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/exception/UnprocessableContentException.java
@@ -0,0 +1,14 @@
+package jp.moreslowly.oi.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
+public class UnprocessableContentException extends RuntimeException {
+
+ public static final String ROOM_IS_NOT_FOUND = "Room is not found.";
+
+ public UnprocessableContentException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/models/Card.java b/src/main/java/jp/moreslowly/oi/models/Card.java
new file mode 100644
index 0000000..5416515
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/models/Card.java
@@ -0,0 +1,87 @@
+package jp.moreslowly.oi.models;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public final class Card implements Serializable {
+ public enum Suit {
+ SPADE("♠️"),
+ HEART("♥️"),
+ DIAMOND("♦️"),
+ CLUB("♣️");
+ private String symbol;
+ private Suit(String symbol) {
+ this.symbol = symbol;
+ }
+ @Override
+ public String toString() {
+ return symbol;
+ }
+ }
+
+ public enum Rank {
+ ACE("A"),
+ TWO("2"),
+ THREE("3"),
+ FOUR("4"),
+ FIVE("5"),
+ SIX("6"),
+ SEVEN("7"),
+ EIGHT("8"),
+ NINE("9"),
+ TEN("10"),
+ JACK("J"),
+ QUEEN("Q"),
+ KING("K");
+ private String rankValue;
+ private Rank(String rank) {
+ this.rankValue = rank;
+ }
+ @Override
+ public String toString() {
+ return rankValue;
+ }
+ public int getPoint() {
+ if (this == ACE) {
+ return 1;
+ } else if (this == JACK) {
+ return 11;
+ } else if (this == QUEEN) {
+ return 12;
+ } else if (this == KING) {
+ return 13;
+ } else {
+ return Integer.parseInt(rankValue);
+ }
+ }
+ }
+
+ private Suit suit;
+ private Rank rank;
+
+ public String toString() {
+ return suit.toString() + rank.toString();
+ }
+
+ public static List generateCardDeck() {
+ List cardDeck = new ArrayList<>();
+ for (Suit suit : Suit.values()) {
+ for (Rank rank : Rank.values()) {
+ cardDeck.add(new Card(suit, rank));
+ }
+ }
+ Collections.shuffle(cardDeck);
+ return cardDeck;
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/models/Member.java b/src/main/java/jp/moreslowly/oi/models/Member.java
new file mode 100644
index 0000000..6c85ada
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/models/Member.java
@@ -0,0 +1,18 @@
+package jp.moreslowly.oi.models;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+@Data
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+public class Member {
+ private String nickname;
+ private UUID id;
+ private LocalDateTime lastAccessedAt;
+}
diff --git a/src/main/java/jp/moreslowly/oi/models/Nickname.java b/src/main/java/jp/moreslowly/oi/models/Nickname.java
new file mode 100644
index 0000000..37ceb33
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/models/Nickname.java
@@ -0,0 +1,25 @@
+package jp.moreslowly.oi.models;
+
+import java.util.Arrays;
+import java.util.List;
+
+public final class Nickname {
+ public static final List NICKNAME_LIST = Arrays.asList(
+ "サクラ",
+ "ウメ",
+ "カエデ",
+ "シラカバ",
+ "アジサイ",
+ "ツツジ",
+ "モミジ",
+ "ツバキ",
+ "マツ",
+ "モクレン",
+ "タケ",
+ "ヒイラギ"
+ );
+ public static final int NICKNAME_LIST_SIZE = NICKNAME_LIST.size();
+
+ private Nickname() {
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java b/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java
new file mode 100644
index 0000000..ad4d174
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/repository/RoomRepository.java
@@ -0,0 +1,13 @@
+package jp.moreslowly.oi.repository;
+
+import java.util.Optional;
+
+import org.springframework.data.repository.CrudRepository;
+
+import jp.moreslowly.oi.dao.Room;
+
+public interface RoomRepository extends CrudRepository {
+ boolean existsById(String id);
+ Optional findById(String id);
+ long count();
+}
diff --git a/src/main/java/jp/moreslowly/oi/service/CardService.java b/src/main/java/jp/moreslowly/oi/service/CardService.java
new file mode 100644
index 0000000..d66b815
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/service/CardService.java
@@ -0,0 +1,9 @@
+package jp.moreslowly.oi.service;
+
+import java.util.List;
+
+import jp.moreslowly.oi.models.Card;
+
+public interface CardService {
+ int evaluate(List cards);
+}
diff --git a/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java b/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java
new file mode 100644
index 0000000..b18040a
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/service/CardServiceImpl.java
@@ -0,0 +1,22 @@
+package jp.moreslowly.oi.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.stereotype.Service;
+
+import jp.moreslowly.oi.exception.InternalErrorException;
+import jp.moreslowly.oi.models.Card;
+
+@Service
+public class CardServiceImpl implements CardService {
+
+ @Override
+ public int evaluate(List cards) {
+ Optional maybeSum = cards.stream().map(card -> card.getRank().getPoint()).reduce((a, b) -> a + b);
+ if (!maybeSum.isPresent()) {
+ throw new InternalErrorException("some thing wrong in point calculation");
+ }
+ return maybeSum.get() % 10;
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/service/RoomService.java b/src/main/java/jp/moreslowly/oi/service/RoomService.java
new file mode 100644
index 0000000..d395fd2
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/service/RoomService.java
@@ -0,0 +1,25 @@
+package jp.moreslowly.oi.service;
+
+import java.util.List;
+
+import org.springframework.web.context.request.async.DeferredResult;
+
+import jakarta.servlet.http.HttpSession;
+import jp.moreslowly.oi.dto.BetDto;
+import jp.moreslowly.oi.dto.RequestCardDto;
+import jp.moreslowly.oi.dto.RoomDto;
+
+public interface RoomService {
+
+ List getRoomIdList();
+
+ RoomDto enterRoom(HttpSession session, String id);
+
+ void subscribe(String id, HttpSession session, DeferredResult deferredResult);
+
+ void reset(String id);
+
+ void bet(HttpSession session, BetDto betDto);
+
+ void requestCard(HttpSession session, RequestCardDto requestOneMoreDto);
+}
diff --git a/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java b/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java
new file mode 100644
index 0000000..5340340
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/service/RoomServiceImpl.java
@@ -0,0 +1,258 @@
+package jp.moreslowly.oi.service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import jakarta.servlet.http.HttpSession;
+import jp.moreslowly.oi.common.RoomLimitation;
+import jp.moreslowly.oi.common.SessionKey;
+import jp.moreslowly.oi.dao.Bet;
+import jp.moreslowly.oi.dao.Room;
+import jp.moreslowly.oi.dao.Room.Status;
+import jp.moreslowly.oi.dto.BetDto;
+import jp.moreslowly.oi.dto.RequestCardDto;
+import jp.moreslowly.oi.dto.RoomDto;
+import jp.moreslowly.oi.exception.FullMemberException;
+import jp.moreslowly.oi.exception.NoRoomException;
+import jp.moreslowly.oi.exception.UnprocessableContentException;
+import jp.moreslowly.oi.models.Card;
+import jp.moreslowly.oi.models.Member;
+import jp.moreslowly.oi.models.Nickname;
+import jp.moreslowly.oi.repository.RoomRepository;
+import jp.moreslowly.oi.tasks.DealerManager;
+import jp.moreslowly.oi.tasks.DealerManager.UpdateStatus;
+import lombok.extern.log4j.Log4j2;
+
+@Service
+@Log4j2
+public class RoomServiceImpl implements RoomService {
+
+ @Autowired
+ private RoomRepository roomRepository;
+
+ @Autowired
+ private DealerManager dealerManager;
+
+ @Override
+ public List getRoomIdList() {
+ List roomIdList = new ArrayList<>();
+ roomRepository.findAll().forEach(room -> roomIdList.add(room.getId()));
+ return roomIdList;
+ }
+
+ private Room findOrCreateRoom(String id) {
+ dealerManager.updateAndNotify(id, () -> {
+ Optional maybeRoom = roomRepository.findById(id);
+ if (maybeRoom.isPresent()) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ if (roomRepository.count() >= RoomLimitation.MAX_ROOM_SIZE) {
+ throw new NoRoomException("No Room");
+ }
+ Room room = Room.builder()
+ .id(id)
+ .status(Status.START)
+ .members(new ArrayList<>())
+ .wallets(new HashMap<>())
+ .lastAccessedAt(LocalDateTime.now())
+ .updatedAt(LocalDateTime.now())
+ .build();
+ room.getWallets().put("dummy", 0);
+ log.info("#### create room {}", id);
+ roomRepository.save(room);
+
+ return UpdateStatus.NOT_UPDATED;
+ });
+
+ Room room = roomRepository.findById(id)
+ .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND));
+ room.setLastAccessedAt(LocalDateTime.now());
+ dealerManager.updateAndNotify(id, () -> {
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ });
+ return room;
+ }
+
+ private UUID getUserId(HttpSession session) {
+ String userId = (String) session.getAttribute(SessionKey.USER_ID);
+ if (Objects.isNull(userId)) {
+ userId = UUID.randomUUID().toString();
+ session.setAttribute(SessionKey.USER_ID, userId);
+ }
+ return UUID.fromString(userId);
+ }
+
+ private String getNickname(HttpSession session, UUID userId, Room room) {
+ dealerManager.updateAndNotify(room.getId(), () -> {
+ if (Objects.isNull(room.getMembers())) {
+ room.setMembers(new ArrayList<>());
+ }
+
+ Optional maybeMember = room.getMembers().stream().filter(m -> m.getId().equals(userId)).findFirst();
+ if (maybeMember.isPresent()) {
+ session.setAttribute(SessionKey.NICKNAME, maybeMember.get().getNickname());
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ List unusedNames = Nickname.NICKNAME_LIST.stream().filter(name -> {
+ return room.getMembers().stream().noneMatch(m -> m.getNickname().equals(name));
+ }).collect(Collectors.toList());
+ if (unusedNames.isEmpty()) {
+ throw new FullMemberException("Nickname is full");
+ }
+ String nickname = unusedNames.get((int) (Math.random() * unusedNames.size()));
+ session.setAttribute(SessionKey.NICKNAME, nickname);
+ if (room.getMembers().size() >= RoomLimitation.MAX_MEMBER_SIZE) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ room.getMembers().add(Member.builder().id(userId).nickname(nickname).build());
+
+ Integer amount = room.getWallets().get(nickname);
+ if (Objects.nonNull(amount)) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ room.getWallets().putIfAbsent(nickname, 10000);
+
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ });
+ return (String) session.getAttribute(SessionKey.NICKNAME);
+ }
+
+ @Override
+ public RoomDto enterRoom(HttpSession session, String id) {
+ String enteredRoomId = (String) session.getAttribute(SessionKey.ROOM_ID);
+ if (Objects.isNull(enteredRoomId)) {
+ session.setAttribute(SessionKey.ROOM_ID, id);
+ enteredRoomId = id;
+ }
+ if (!enteredRoomId.equals(id)) {
+ session.removeAttribute(SessionKey.NICKNAME);
+ session.setAttribute(SessionKey.ROOM_ID, id);
+ }
+
+ Room room = findOrCreateRoom(id);
+ UUID userId = getUserId(session);
+ String nickname = getNickname(session, userId, room);
+
+ return RoomDto.fromEntity(room, nickname);
+ }
+
+ private ExecutorService runners = Executors
+ .newFixedThreadPool(RoomLimitation.MAX_ROOM_SIZE * RoomLimitation.MAX_MEMBER_SIZE);
+
+ @Override
+ public void subscribe(String id, HttpSession session, DeferredResult deferredResult) {
+ Room room = findOrCreateRoom(id);
+ UUID userId = getUserId(session);
+ String yourName = getNickname(session, userId, room);
+
+ dealerManager.updateAndNotify(room.getId(), () -> {
+ room.setLastAccessedAt(LocalDateTime.now());
+ room.getMembers().stream().filter(m -> m.getId().equals(userId)).findFirst().ifPresent(m -> {
+ m.setLastAccessedAt(LocalDateTime.now());
+ });
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ });
+
+ runners.execute(() -> {
+ dealerManager.waitForUpdating(id, () -> {
+ Room newRoom = roomRepository.findById(id)
+ .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND));
+ RoomDto dto = RoomDto.fromEntity(newRoom, yourName);
+ deferredResult.setResult(dto);
+ });
+ });
+ }
+
+ @Override
+ public void reset(String id) {
+ dealerManager.updateAndNotify(id, () -> {
+ Room room = roomRepository.findById(id)
+ .orElseThrow(() -> new UnprocessableContentException(UnprocessableContentException.ROOM_IS_NOT_FOUND));
+ room.reset();
+ room.setMembers(new ArrayList<>());
+ room.setUpdatedAt(LocalDateTime.now());
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ });
+ }
+
+ @Override
+ public void bet(HttpSession session, BetDto betDto) {
+ UUID userId = getUserId(session);
+ dealerManager.updateAndNotify(betDto.getRoomId(), () -> {
+ Optional maybeRoom = roomRepository.findById(betDto.getRoomId());
+ if (!maybeRoom.isPresent()) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ Room room = maybeRoom.get();
+ if (!room.getMembers().stream().anyMatch(m -> m.getId().equals(userId))) {
+ throw new UnprocessableContentException("Invalid nickname");
+ }
+
+ if (CollectionUtils.isEmpty(room.getBets())) {
+ room.setBets(new ArrayList<>());
+ }
+ Integer wallet = room.getWallets().get(betDto.getUserName());
+ wallet -= betDto.getBetAmount();
+ room.getWallets().put(betDto.getUserName(), wallet);
+ room.getBets().add(betDto.toEntity());
+ roomRepository.save(room);
+
+ return UpdateStatus.UPDATED;
+ });
+ }
+
+ @Override
+ public void requestCard(HttpSession session, RequestCardDto requestOneMoreDto) {
+ UUID userId = getUserId(session);
+ dealerManager.updateAndNotify(requestOneMoreDto.getRoomId(), () -> {
+ Optional maybeRoom = roomRepository.findById(requestOneMoreDto.getRoomId());
+ if (!maybeRoom.isPresent()) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ Room room = maybeRoom.get();
+ if (!room.getMembers().stream().anyMatch(m -> m.getId().equals(userId))) {
+ throw new UnprocessableContentException("You are not member of this room");
+ }
+
+ Optional maybeBet = room.getBets().stream().filter(
+ bet -> Objects.equals(bet.getHandIndex(), requestOneMoreDto.getHandIndex())).findFirst();
+ if (!maybeBet.isPresent()) {
+ throw new UnprocessableContentException("You are not owner of this hand");
+ }
+ Bet bet = maybeBet.get();
+ if (!bet.getUserName().equals(requestOneMoreDto.getUserName())) {
+ throw new UnprocessableContentException("You are not owner of this hand: " + bet.getUserName() + ", " +
+ requestOneMoreDto.getUserName());
+ }
+
+ List hands = room.getHandsAt(requestOneMoreDto.getHandIndex());
+ if (hands.size() == 3) {
+ throw new UnprocessableContentException("You can't request more card");
+ }
+ Card aCard = room.getDeck().remove(0);
+ hands.add(aCard);
+
+ roomRepository.save(room);
+
+ return UpdateStatus.UPDATED;
+ });
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java b/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java
new file mode 100644
index 0000000..3533a71
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/tasks/DealerManager.java
@@ -0,0 +1,79 @@
+package jp.moreslowly.oi.tasks;
+
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import jp.moreslowly.oi.common.RoomLimitation;
+import jp.moreslowly.oi.repository.RoomRepository;
+import jp.moreslowly.oi.service.CardService;
+
+public class DealerManager {
+
+ public enum UpdateStatus {
+ UPDATED,
+ NOT_UPDATED
+ }
+
+ public interface BeNotified {
+ void afterUpdate();
+ }
+
+ public interface Updatable {
+ UpdateStatus update();
+ }
+
+ @Autowired
+ private RoomRepository roomRepository;
+
+ @Autowired
+ private CardService cardService;
+
+ private ConcurrentHashMap lockMap = new ConcurrentHashMap<>();
+
+ private ExecutorService runners = Executors
+ .newFixedThreadPool(RoomLimitation.MAX_ROOM_SIZE);
+
+ private Object getLock(String roomId) {
+ return lockMap.computeIfAbsent(roomId, k -> new Object());
+ }
+
+ public CardService getCardService() {
+ return cardService;
+ }
+
+ public void waitForUpdating(String roomId, BeNotified proc) {
+ Object lock = getLock(roomId);
+ synchronized (lock) {
+ try {
+ lock.wait();
+ } catch (InterruptedException e) {
+ // do nothing
+ }
+ proc.afterUpdate();
+ }
+ }
+
+ public void updateAndNotify(String roomId, Updatable proc) {
+ Object lock = getLock(roomId);
+ synchronized (lock) {
+ if (proc.update() == UpdateStatus.UPDATED) {
+ lock.notifyAll();
+ }
+ }
+ }
+
+ @Scheduled(fixedRate = 1000)
+ public void startDealer() {
+ roomRepository.findAll().forEach(room -> {
+ if (Objects.isNull(room)) {
+ return;
+ }
+ runners.submit(new DealerTask(this, roomRepository, room.getId()));
+ });
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java b/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java
new file mode 100644
index 0000000..88288a8
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/tasks/DealerTask.java
@@ -0,0 +1,246 @@
+package jp.moreslowly.oi.tasks;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.springframework.util.CollectionUtils;
+
+import jp.moreslowly.oi.common.RoomLimitation;
+import jp.moreslowly.oi.dao.Bet;
+import jp.moreslowly.oi.dao.Room;
+import jp.moreslowly.oi.models.Card;
+import jp.moreslowly.oi.repository.RoomRepository;
+import jp.moreslowly.oi.tasks.DealerManager.UpdateStatus;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+public class DealerTask implements Runnable {
+
+ private DealerManager manager;
+ private RoomRepository roomRepository;
+ private String roomId;
+
+ public DealerTask(DealerManager manager, RoomRepository roomRepository, String roomId) {
+ this.manager = manager;
+ this.roomRepository = roomRepository;
+ this.roomId = roomId;
+ }
+
+ @Override
+ public void run() {
+ manager.updateAndNotify(roomId, () -> {
+ Optional maybeRoom = roomRepository.findById(roomId);
+ if (!maybeRoom.isPresent()) {
+ return UpdateStatus.NOT_UPDATED;
+ }
+ Room room = maybeRoom.get();
+
+ switch (room.getStatus()) {
+ case START:
+ return processStart(room);
+ case SHUFFLE:
+ return processShuffle(room);
+ case HAND_OUT_CARDS:
+ return processHandOutCards(room);
+ case WAIT_TO_BET:
+ return processWaitToBet(room);
+ case WAIT_TO_REQUEST:
+ return processWaitToRequest(room);
+ case DEALER_TURN:
+ return processDealerTurn(room);
+ case LIQUIDATION:
+ return processLiquidation(room);
+ case END:
+ return processEnd(room);
+ default:
+ return UpdateStatus.NOT_UPDATED;
+ }
+ });
+ }
+
+ private static final int SHORT_TIMEOUT_SEC = 5;
+ private static final int SHUFFLE_TIMEOUT_SEC = 3;
+ private static final int GENERAL_TIMEOUT_SEC = 30;
+ private static final int MIDDLE_TIMEOUT_SEC = 20;
+
+ private UpdateStatus processStart(Room room) {
+ log.info("processStart: room id {}", room.getId());
+ room.reset();
+ room.setStatus(Room.Status.START.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft((long) SHUFFLE_TIMEOUT_SEC);
+ room.setTimeLeftDenominator((long) SHUFFLE_TIMEOUT_SEC);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processShuffle(Room room) {
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(SHUFFLE_TIMEOUT_SEC);
+ if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) {
+ room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit));
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ List deck = Card.generateCardDeck();
+ room.setDeck(deck);
+ room.setStatus(Room.Status.SHUFFLE.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processHandOutCards(Room room) {
+ List> hands = new ArrayList<>();
+ for (int i = 0; i < RoomLimitation.MAX_HAND_OUT_SIZE; i++) {
+ List hand = new ArrayList<>();
+ for (int j = 0; j < 2; j++) {
+ hand.add(room.getDeck().remove(0));
+ }
+ hands.add(hand);
+ }
+
+ room.setHands1(hands.get(0));
+ room.setHands2(hands.get(1));
+ room.setHands3(hands.get(2));
+ room.setHands4(hands.get(3));
+ room.setHands5(hands.get(4));
+ room.setHands6(hands.get(5));
+ room.setHands7(hands.get(6));
+ room.setStatus(Room.Status.HAND_OUT_CARDS.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft((long) GENERAL_TIMEOUT_SEC);
+ room.setTimeLeftDenominator((long) GENERAL_TIMEOUT_SEC);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processWaitToBet(Room room) {
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(GENERAL_TIMEOUT_SEC);
+ if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) {
+ room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit));
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ room.setStatus(Room.Status.WAIT_TO_BET.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft((long) GENERAL_TIMEOUT_SEC);
+ room.setTimeLeftDenominator((long) GENERAL_TIMEOUT_SEC);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processWaitToRequest(Room room) {
+ if (CollectionUtils.isEmpty(room.getBets())) {
+ room.setStatus(Room.Status.WAIT_TO_REQUEST.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft((long) SHORT_TIMEOUT_SEC);
+ room.setTimeLeftDenominator((long) SHORT_TIMEOUT_SEC);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(GENERAL_TIMEOUT_SEC);
+ if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) {
+ room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit));
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ room.setStatus(Room.Status.WAIT_TO_REQUEST.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft((long) SHORT_TIMEOUT_SEC);
+ room.setTimeLeftDenominator((long) SHORT_TIMEOUT_SEC);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processDealerTurn(Room room) {
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(SHORT_TIMEOUT_SEC);
+ if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) {
+ room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit));
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ List parentCards = room.getHands7();
+ int point = manager.getCardService().evaluate(parentCards);
+ int threshold = Math.random() < 0.5 ? 5 : 6;
+ if (point <= threshold) {
+ parentCards.add(room.getDeck().remove(0));
+ room.setHands7(parentCards);
+ }
+
+ room.setStatus(Room.Status.DEALER_TURN.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processLiquidation(Room room) {
+ List parentCards = room.getHands7();
+ int parentPoint = manager.getCardService().evaluate(parentCards);
+ List bets = room.getBets();
+ if (!CollectionUtils.isEmpty(bets)) {
+ room.getBets().stream().forEach(bet -> {
+ List hands = room.getHandsAt(bet.getHandIndex());
+ int point = manager.getCardService().evaluate(hands);
+ if (point > parentPoint) {
+ bet.setResult(Bet.Result.WIN);
+ int betAmount = bet.getBetAmount();
+ int wallet = room.getWallets().get(bet.getUserName());
+ room.getWallets().put(bet.getUserName(), wallet + betAmount * 2);
+ } else if (point == parentPoint) {
+ bet.setResult(Bet.Result.DRAW);
+ int betAmount = bet.getBetAmount();
+ int wallet = room.getWallets().get(bet.getUserName());
+ room.getWallets().put(bet.getUserName(), wallet + betAmount);
+ } else {
+ bet.setResult(Bet.Result.LOSE);
+ }
+ });
+ }
+
+ room.setStatus(Room.Status.LIQUIDATION.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ int timeout = MIDDLE_TIMEOUT_SEC;
+ if (CollectionUtils.isEmpty(room.getBets())) {
+ timeout = SHORT_TIMEOUT_SEC;
+ }
+ room.setTimeLeft((long) timeout);
+ room.setTimeLeftDenominator((long) timeout);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+
+ private UpdateStatus processEnd(Room room) {
+ int timeout = MIDDLE_TIMEOUT_SEC;
+ if (CollectionUtils.isEmpty(room.getBets())) {
+ timeout = SHORT_TIMEOUT_SEC;
+ }
+ LocalDateTime now = LocalDateTime.now();
+ LocalDateTime timeLimit = room.getUpdatedAt().plusSeconds(timeout);
+ if (Objects.nonNull(room.getUpdatedAt()) && now.isBefore(timeLimit)) {
+ room.setTimeLeft(ChronoUnit.SECONDS.between(now, timeLimit));
+ roomRepository.save(room);
+ return UpdateStatus.NOT_UPDATED;
+ }
+
+ room.reset();
+ room.setStatus(Room.Status.END.next());
+ room.setUpdatedAt(LocalDateTime.now());
+ room.setTimeLeft(0L);
+ room.setTimeLeftDenominator(1L);
+ roomRepository.save(room);
+ return UpdateStatus.UPDATED;
+ }
+}
diff --git a/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java b/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java
new file mode 100644
index 0000000..ee6f81e
--- /dev/null
+++ b/src/main/java/jp/moreslowly/oi/tasks/SweeperTask.java
@@ -0,0 +1,44 @@
+package jp.moreslowly.oi.tasks;
+
+import java.time.LocalDateTime;
+import java.util.Objects;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import jp.moreslowly.oi.repository.RoomRepository;
+import lombok.extern.log4j.Log4j2;
+
+@Log4j2
+public class SweeperTask {
+
+ private static final int EXPIRED_MINUTES = 5;
+ private static final int SWEEP_FIXED_DELAY = 10 * 1000;
+
+ @Autowired private RoomRepository roomRepository;
+
+ @Scheduled(fixedDelay = SWEEP_FIXED_DELAY)
+ public void sweep() {
+ roomRepository.findAll().forEach(room -> {
+ if (room.getLastAccessedAt().plusMinutes(EXPIRED_MINUTES).isBefore(LocalDateTime.now())) {
+ log.info("### Sweeping room: {}", room.getId());
+ roomRepository.delete(room);
+ }
+
+ room.getMembers().removeIf(member -> {
+ if (Objects.nonNull(member.getLastAccessedAt()) && member.getLastAccessedAt().plusMinutes(EXPIRED_MINUTES).isBefore(LocalDateTime.now())) {
+ log.info("### Sweeping member: {}", member.getNickname());
+ return true;
+ }
+ return false;
+ });
+
+ if (room.getMembers().isEmpty()) {
+ log.info("### Sweeping room: {}", room.getId());
+ roomRepository.delete(room);
+ } else {
+ roomRepository.save(room);
+ }
+ });
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b13789..0342fb2 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,5 @@
+ajp.secret=${AJP_SECRET:password}
+spring.session.store-type=redis
+server.servlet.session.timeout=5184000
+spring.mvc.async.request-timeout=2m
diff --git a/src/main/resources/conf/logback-access.xml b/src/main/resources/conf/logback-access.xml
new file mode 100644
index 0000000..8bc5dbb
--- /dev/null
+++ b/src/main/resources/conf/logback-access.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ combined
+
+
+
+
+
+