pg-mvc-service 2.0.42 → 2.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/assets/favicon.ico +0 -0
  2. package/dist/models/TableDoc.js +4 -1
  3. package/package.json +8 -2
  4. package/index.d.ts +0 -189
  5. package/src/PoolManager.ts +0 -48
  6. package/src/Service.ts +0 -276
  7. package/src/Utils/DateTimeUtil.ts +0 -146
  8. package/src/Utils/NumberUtil.ts +0 -23
  9. package/src/Utils/StringUtil.ts +0 -33
  10. package/src/clients/AwsS3Client.ts +0 -310
  11. package/src/clients/Base64Client.ts +0 -305
  12. package/src/clients/EncryptClient.ts +0 -100
  13. package/src/clients/StringClient.ts +0 -19
  14. package/src/cron/BaseCron.ts +0 -122
  15. package/src/cron/CronExecuter.ts +0 -34
  16. package/src/cron/CronType.ts +0 -25
  17. package/src/documents/Swagger.ts +0 -105
  18. package/src/exceptions/Exception.ts +0 -72
  19. package/src/index.ts +0 -23
  20. package/src/models/ExpressionClient.ts +0 -72
  21. package/src/models/MigrateDatabase.ts +0 -135
  22. package/src/models/MigrateRollback.ts +0 -151
  23. package/src/models/MigrateTable.ts +0 -56
  24. package/src/models/SqlUtils/SelectExpression.ts +0 -97
  25. package/src/models/SqlUtils/UpdateExpression.ts +0 -29
  26. package/src/models/SqlUtils/ValidateValueUtil.ts +0 -354
  27. package/src/models/SqlUtils/WhereExpression.ts +0 -421
  28. package/src/models/TableDoc.ts +0 -366
  29. package/src/models/TableModel.ts +0 -701
  30. package/src/models/Type.ts +0 -62
  31. package/src/models/Utils/MessageUtil.ts +0 -60
  32. package/src/models/ValidateClient.ts +0 -182
  33. package/src/reqestResponse/ReqResType.ts +0 -170
  34. package/src/reqestResponse/RequestType.ts +0 -918
  35. package/src/reqestResponse/ResponseType.ts +0 -420
  36. package/tsconfig.json +0 -14
@@ -1,305 +0,0 @@
1
- import axios from 'axios';
2
- import { PDFDocument } from 'pdf-lib';
3
- import sharp from 'sharp';
4
- import { ValidateStringUtil } from 'type-utils-n-daira';
5
-
6
- export type TPng = 'image/png';
7
- export type TJpeg = 'image/jpeg';
8
- export type TGif = 'image/gif';
9
- export type TImage = TPng | TJpeg | TGif;
10
-
11
- export type TPdf = 'application/pdf';
12
- export type TJson = 'application/json';
13
-
14
- export class Base64Client {
15
- public readonly PREFIX_JPEG_DATA = '/9j/';
16
- public readonly PREFIX_PNG_DATA = 'iVBORw0KGgo';
17
-
18
- constructor() { }
19
-
20
- // public encode(text: string): string {
21
- // return Buffer.from(text).toString('base64');
22
- // }
23
-
24
- public tryDecode(base64: string): Buffer<ArrayBuffer> | false {
25
- try {
26
- // Data URLのパターンをチェック
27
- if (base64.startsWith('data:')) {
28
- const matches = base64.match(/^data:([A-Za-z-+/]+);base64,(.+)$/);
29
- if (!matches || matches.length !== 3) {
30
- return false;
31
- }
32
- // base64部分のみを取得
33
- base64 = matches[2];
34
- }
35
-
36
- if (base64.length % 4 !== 0) {
37
- return false;
38
- }
39
-
40
- const regex = /^[A-Za-z0-9+/]*={0,2}$/;
41
- if (!regex.test(base64)) {
42
- return false;
43
- }
44
-
45
- return Buffer.from(base64, 'base64');
46
- } catch {
47
- return false;
48
- }
49
- }
50
-
51
- public getMimeType(data: string | Buffer<ArrayBuffer>): TImage | TPdf {
52
- try {
53
- let buffer;
54
- if (typeof data === 'string') {
55
- // Data URLのパターンをチェック
56
- if (data.startsWith('data:')) {
57
- const matches = data.match(/^data:([A-Za-z-+/]+);base64,(.+)$/);
58
- if (!matches || matches.length !== 3) {
59
- throw new Error('Invalid Data URL format');
60
- }
61
- // base64部分のみを取得
62
- data = matches[2];
63
- }
64
- buffer = this.tryDecode(data);
65
- } else {
66
- buffer = data;
67
- }
68
-
69
- if (buffer === false) {
70
- throw new Error('Cannot getMineType because the input is not in base64 format.');
71
- }
72
-
73
- const header = buffer.subarray(0, 4);
74
- if (header[0] === 0x25 && header[1] === 0x50 && header[2] === 0x44 && header[3] === 0x46) {
75
- return 'application/pdf';
76
- } else if (header[0] === 0x89 && header[1] === 0x50 && header[2] === 0x4E && header[3] === 0x47) {
77
- return 'image/png';
78
- } else if (header[0] === 0xFF && header[1] === 0xD8) {
79
- return 'image/jpeg';
80
- } else if (header[0] === 0x47 && header[1] === 0x49 && header[2] === 0x46 && header[3] === 0x38) {
81
- return 'image/gif';
82
- }
83
-
84
- throw new Error('Cannot getMimeType because the file type is not PDF, PNG, JPEG, or GIF.');
85
- } catch {
86
- throw new Error('Cannot getMineType because the input is not in base64 format.');
87
- }
88
- }
89
-
90
- public async mergeToPdfBase64(datas: string[]): Promise<string> {
91
- const mergedPdf = await PDFDocument.create();
92
-
93
- for (const data of datas) {
94
- const buffer = this.tryDecode(data);
95
- if (buffer === false) {
96
- throw new Error('Cannot mergeToPdf because the input is not in base64 format.');
97
- }
98
-
99
- const fileType = this.getMimeType(buffer);
100
- if (fileType === 'application/pdf') {
101
- const pdfDoc = await PDFDocument.load(buffer);
102
- const pages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
103
- pages.forEach(page => mergedPdf.addPage(page));
104
- } else {
105
- // convert from image to pdf
106
- const imagePdf = await this.convertImageToPdf(buffer);
107
- const pdfDoc = await PDFDocument.load(imagePdf);
108
- const pages = await mergedPdf.copyPages(pdfDoc, pdfDoc.getPageIndices());
109
- pages.forEach(page => mergedPdf.addPage(page));
110
- }
111
- }
112
-
113
- // 結合したPDFをBase64に変換
114
- const mergedPdfBytes = await mergedPdf.save();
115
- return Buffer.from(mergedPdfBytes).toString('base64');
116
- }
117
-
118
- private async convertImageToPdf(imageBuffer: Buffer): Promise<Buffer> {
119
- const fileType = this.getMimeType(imageBuffer);
120
-
121
- let optimizedImage;
122
- if (fileType === 'image/gif') {
123
- // gifの場合はpngに変換して処理
124
- optimizedImage = await sharp(imageBuffer)
125
- .png()
126
- .toBuffer();
127
- } else {
128
- optimizedImage = await sharp(imageBuffer).toBuffer();
129
- }
130
-
131
- // 新しいPDFドキュメントを作成
132
- const pdfDoc = await PDFDocument.create();
133
- const page = pdfDoc.addPage();
134
-
135
- // 画像をPDFに埋め込み
136
-
137
- let image;
138
- if (fileType === 'image/jpeg') {
139
- image = await pdfDoc.embedJpg(optimizedImage);
140
- } else {
141
- image = await pdfDoc.embedPng(optimizedImage);
142
- }
143
- const { width, height } = image.scale(1);
144
-
145
- // ページサイズを画像に合わせる
146
- page.setSize(width, height);
147
-
148
- // 画像を描画
149
- page.drawImage(image, {
150
- x: 0,
151
- y: 0,
152
- width,
153
- height,
154
- });
155
-
156
- // PDFをバッファに変換
157
- const pdfBytes = await pdfDoc.save();
158
- return Buffer.from(pdfBytes);
159
- }
160
-
161
- public isJpeg(value: any): value is string {
162
- if (ValidateStringUtil.isBase64(value) === false) {
163
- return false
164
- }
165
-
166
- if (value.startsWith('data:')) {
167
- if (value.startsWith('data:image/jpeg,') === false && value.startsWith('data:image/jpg,') === false) {
168
- return false;
169
- }
170
-
171
- const valueParts = value.split(',');
172
- if (valueParts.length !== 2) {
173
- return false;
174
- }
175
-
176
- return valueParts[1].startsWith(this.PREFIX_JPEG_DATA);
177
- }
178
-
179
- return value.startsWith(this.PREFIX_JPEG_DATA);
180
- }
181
-
182
- public isPng(value: any): value is string {
183
- if (ValidateStringUtil.isBase64(value) === false) {
184
- return false
185
- }
186
-
187
- if (value.startsWith('data:')) {
188
- if (value.startsWith('data:image/png,') === false) {
189
- return false;
190
- }
191
-
192
- const valueParts = value.split(',');
193
- if (valueParts.length !== 2) {
194
- return false;
195
- }
196
-
197
- return valueParts[1].startsWith(this.PREFIX_PNG_DATA);
198
- }
199
-
200
- return value.startsWith(this.PREFIX_PNG_DATA);
201
- }
202
-
203
- public async tryConvertToPng(base64Value: any): Promise<string | false> {
204
- if (ValidateStringUtil.isBase64(base64Value) === false) {
205
- return false;
206
- }
207
-
208
- const base64Data = base64Value.startsWith('data:') ? base64Value.split(',')[1] : base64Value;
209
- if (this.isPng(base64Data)) {
210
- return base64Data;
211
- } else if (this.isJpeg(base64Data)) {
212
- const buffer = Buffer.from(base64Data, 'base64');
213
- try {
214
- const pngBuffer = await sharp(buffer)
215
- .ensureAlpha().png().toBuffer();
216
- return pngBuffer.toString('base64');
217
- } catch (e) {
218
- return false;
219
- }
220
- }
221
-
222
- return false;
223
- }
224
-
225
- public async resizeImage(base64Data: string, toSize: {w: number} | {h: number} | {w: number; h: number; func: 'max' | 'min'}| {rate: number}): Promise<string> {
226
- if (ValidateStringUtil.isBase64(base64Data) === false) {
227
- throw new Error("The specified data is not in base64 format");
228
- }
229
-
230
- const imageBuffer = Buffer.from(base64Data, 'base64');
231
- const metadata = await sharp(imageBuffer).metadata();
232
- const { width, height, format } = metadata;
233
- if (width === undefined || height === undefined) {
234
- throw new Error("Failed to retrieve image dimensions");
235
- }
236
-
237
- let rate = 1;
238
- if ('rate' in toSize) {
239
- rate = toSize.rate;
240
- } else if ('w' in toSize && 'h' in toSize && 'func' in toSize) {
241
- const wRate = toSize.w / width;
242
- const hRate = toSize.h / height;
243
- switch (toSize.func) {
244
- case 'max':
245
- rate = Math.max(wRate, hRate);
246
- break;
247
- case 'min':
248
- rate = Math.min(wRate, hRate);
249
- break;
250
- }
251
-
252
- } else if ('w' in toSize) {
253
- rate = toSize.w / width;
254
- } else if ('h' in toSize) {
255
- rate = toSize.h / height;
256
- }
257
-
258
- // 画像は1倍より大きくできないので
259
- if (rate >= 1 || rate <= 0) {
260
- return base64Data;
261
- }
262
-
263
- let resizedImage: Buffer;
264
-
265
- // フォーマットに応じて処理を分岐
266
- const targetWidth = Math.round(width * rate);
267
- const targetHeight = Math.round(height * rate);
268
- if (format === 'png') {
269
- resizedImage = await sharp(imageBuffer)
270
- .resize(targetWidth, targetHeight, {
271
- fit: 'inside',
272
- withoutEnlargement: true
273
- })
274
- .png({ quality: 90 })
275
- .toBuffer();
276
- } else {
277
- // JPEG、その他のフォーマット
278
- resizedImage = await sharp(imageBuffer)
279
- .resize(targetWidth, targetHeight, {
280
- fit: 'inside',
281
- withoutEnlargement: true
282
- })
283
- .jpeg({ quality: 90 })
284
- .toBuffer();
285
- }
286
-
287
- return resizedImage.toString('base64');
288
- }
289
-
290
- public async fetchImageAsBase64(imageUrl: string): Promise<string> {
291
- const res = await axios.get(imageUrl, {
292
- responseType: 'arraybuffer' // これを追加
293
- });
294
-
295
- // Content-Typeをチェック
296
- const contentType = res.headers['content-type'];
297
- if (!contentType?.startsWith('image/')) {
298
- throw new Error(`Invalid content type: ${contentType}. Expected image/*`);
299
- }
300
-
301
- // ArrayBufferをBufferに変換してBase64にエンコード
302
- const buffer = Buffer.from(res.data);
303
- return buffer.toString('base64');
304
- }
305
- }
@@ -1,100 +0,0 @@
1
- import crypto from 'crypto';
2
-
3
- export class EncryptClient {
4
- private secretKeyHex?: string;
5
- get SecretKey(): Buffer<ArrayBuffer> {
6
- if (this.secretKeyHex === undefined) {
7
- throw new Error("Please set the secret key.");
8
- }
9
- return Buffer.from(this.secretKeyHex, 'hex');
10
- }
11
- private hmacKeyBase64?: string;
12
- get HmacKey(): Buffer<ArrayBuffer> {
13
- if (this.hmacKeyBase64 === undefined) {
14
- throw new Error("Please set the hmac key.");
15
- }
16
- return Buffer.from(this.hmacKeyBase64, 'base64');
17
- }
18
-
19
- constructor(params: {secretKeyHex?: string, hmacKeyBase64?: string}) {
20
- this.secretKeyHex = params?.secretKeyHex;
21
- this.hmacKeyBase64 = params?.hmacKeyBase64;
22
- }
23
-
24
- public encryptAndSign(data: string | {[key: string]: any}): string {
25
- const iv = crypto.randomBytes(12);
26
- const cipher = crypto.createCipheriv('aes-256-gcm', this.SecretKey, iv);
27
-
28
- const plaintext = typeof data === 'string' ? data : JSON.stringify(data);
29
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
30
- const authTag = cipher.getAuthTag();
31
-
32
- const combined = Buffer.concat([iv, authTag, encrypted]);
33
- const payload = this.base64urlEncode(combined);
34
-
35
- const hmac = crypto.createHmac('sha256', this.HmacKey).update(payload).digest();
36
- const signature = this.base64urlEncode(hmac);
37
-
38
- return `${payload}.${signature}`;
39
- }
40
-
41
- public isValidToken(token: string): boolean {
42
- // 形式チェック、.で区切られているか?
43
- const [payload, signature] = token.split('.');
44
- if (!payload || !signature) {
45
- return false;
46
- }
47
-
48
- // 改竄チェック
49
- const expectedSig = crypto.createHmac('sha256', this.HmacKey).update(payload).digest();
50
- const expectedSigStr = this.base64urlEncode(expectedSig);
51
-
52
- if (signature !== expectedSigStr) {
53
- return false;
54
- }
55
-
56
- return true;
57
- }
58
-
59
- public decrypt<T>(token: string): T {
60
- // 形式チェック、.で区切られているか?
61
- const [payload, signature] = token.split('.');
62
- if (!payload || !signature) {
63
- throw new Error('Invalid token format');
64
- }
65
-
66
- // 改竄チェック
67
- const expectedSig = crypto.createHmac('sha256', this.HmacKey).update(payload).digest();
68
- const expectedSigStr = this.base64urlEncode(expectedSig);
69
- if (signature !== expectedSigStr){
70
- throw new Error('The token appears to have been tampered with');
71
- }
72
-
73
- const combined = this.base64urlDecode(payload);
74
- const iv = combined.subarray(0, 12);
75
- const authTag = combined.subarray(12, 28);
76
- const encrypted = combined.subarray(28);
77
-
78
- const decipher = crypto.createDecipheriv('aes-256-gcm', this.SecretKey, iv);
79
- decipher.setAuthTag(authTag);
80
-
81
- const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
82
- return JSON.parse(decrypted.toString('utf8')) as T;
83
- }
84
-
85
-
86
- private base64urlEncode(buffer: Buffer): string {
87
- return buffer.toString('base64')
88
- .replace(/\+/g, '-')
89
- .replace(/\//g, '_')
90
- .replace(/=+$/, '');
91
- }
92
-
93
- private base64urlDecode(str: string): Buffer {
94
- str = str.replace(/-/g, '+').replace(/_/g, '/');
95
- while (str.length % 4 !== 0) {
96
- str += '=';
97
- }
98
- return Buffer.from(str, 'base64');
99
- }
100
- }
@@ -1,19 +0,0 @@
1
- import { randomBytes } from 'crypto';
2
- import StringUtil from '../Utils/StringUtil';
3
-
4
- export class StringClient {
5
- constructor() { }
6
-
7
- public generateUUIDv7(): string {
8
- const timestamp = BigInt(Date.now()) * BigInt(10000) + BigInt(process.hrtime.bigint() % BigInt(10000));
9
- const timeHex = timestamp.toString(16).padStart(16, '0');
10
-
11
- const randomHex = randomBytes(8).toString('hex');
12
-
13
- return `${timeHex.slice(0, 8)}-${timeHex.slice(8, 12)}-7${timeHex.slice(13, 16)}-${randomHex.slice(0, 4)}-${randomHex.slice(4)}`;
14
- }
15
-
16
- public isUUID(value: any) {
17
- return StringUtil.isUUID(value);
18
- }
19
- }
@@ -1,122 +0,0 @@
1
- import { Pool, PoolClient } from "pg";
2
- import { DateType, DayType, HourType, MinuteSecondType, MonthType } from "./CronType";
3
- import PoolManager from "../PoolManager";
4
- import { AwsS3Client } from "../clients/AwsS3Client";
5
- import { Base64Client } from "../clients/Base64Client";
6
-
7
- export class BaseCron {
8
-
9
- protected readonly isTest: boolean = process.env.NODE_ENV === 'test';
10
- protected dbUser?: string = this.isTest ? process.env.TEST_DB_USER : process.env.DB_USER;
11
- protected dbHost?: string = this.isTest ? process.env.TEST_DB_HOST : process.env.DB_HOST;
12
- protected dbName?: string = this.isTest ? process.env.TEST_DB_DATABASE : process.env.DB_DATABASE;
13
- protected dbPassword?: string = this.isTest ? process.env.TEST_DB_PASSWORD : process.env.DB_PASSWORD;
14
- protected dbPort?: string | number = this.isTest ? process.env.TEST_DB_PORT : process.env.DB_PORT;
15
- protected dbIsSslConnect: boolean = (this.isTest ? process.env.TEST_DB_IS_SSL : process.env.DB_IS_SSL) === 'true';
16
-
17
- private isExecuteRollback: boolean = false;
18
- private pool?: Pool;
19
- private client?: PoolClient;
20
- protected get Client(): PoolClient {
21
- if (this.client === undefined) {
22
- throw new Error("Please call this.PoolClient after using the setClient method.");
23
- }
24
- return this.client;
25
- }
26
-
27
- protected async commit(): Promise<void> {
28
- await this.Client.query('COMMIT');
29
- this.isExecuteRollback = false;
30
- }
31
-
32
- protected async rollback(): Promise<void> {
33
- if (this.isExecuteRollback) {
34
- await this.Client.query('ROLLBACK');
35
- }
36
- this.isExecuteRollback = false;
37
- }
38
-
39
- // **********************************************************************
40
- // こちらのメソッド、プロパティを各サブクラスで設定してください
41
- // **********************************************************************
42
- protected cronCode: string = '';
43
- protected minute: MinuteSecondType | '*' = '*';
44
- protected hour: HourType | '*' = '*';
45
- protected date: DateType | '*' = '*';
46
- protected month: MonthType | '*' = '*';
47
- protected day: DayType | '*' = '*';
48
- public async run() { }
49
-
50
- // **********************************************************************
51
- // ベースクラスで設定
52
- // **********************************************************************
53
- get CronSchedule(): string {
54
- let schedule = '';
55
- schedule += this.minute.toString() + ' ';
56
- schedule += this.hour.toString() + ' ';
57
- schedule += this.date.toString() + ' ';
58
- schedule += this.month.toString() + ' ';
59
- schedule += this.day.toString();
60
- return schedule;
61
- }
62
- get CronCode(): string { return this.cronCode; }
63
-
64
- public async setUp() {
65
- if (this.dbUser === undefined) {
66
- throw new Error("Database user is not configured");
67
- }
68
- if (this.dbHost === undefined) {
69
- throw new Error("Database host is not configured");
70
- }
71
- if (this.dbName === undefined) {
72
- throw new Error("Database name is not configured");
73
- }
74
- if (this.dbPassword === undefined) {
75
- throw new Error("Database password is not configured");
76
- }
77
- if (this.dbPort === undefined) {
78
- throw new Error("Database port is not configured");
79
- }
80
-
81
- this.pool = PoolManager.getPool(this.dbUser, this.dbHost, this.dbName, this.dbPassword, this.dbPort, this.dbIsSslConnect);
82
- this.pool.query(`SET TIME ZONE '${process.env.TZ ?? 'Asia/Tokyo'}';`);
83
- this.client = await this.pool.connect();
84
- await this.Client.query('BEGIN');
85
- this.isExecuteRollback = true;
86
- }
87
-
88
- public async tearDown() {
89
- try {
90
- if (this.isExecuteRollback === false) {
91
- await this.rollback();
92
- }
93
- } finally {
94
- // クライアント接続をリリース
95
- if (this.client) {
96
- this.client.release();
97
- this.client = undefined;
98
- }
99
- }
100
- }
101
-
102
- private s3Client?: AwsS3Client;
103
- get S3Client(): AwsS3Client {
104
- if (this.s3Client === undefined) {
105
- this.s3Client = new AwsS3Client({
106
- bucketName: process.env.S3_BUCKET_NAME,
107
- region: process.env.S3_REGION,
108
- accessKeyId: process.env.S3_ACCESS_KEY_ID,
109
- secretAccessKey: process.env.S3_SECRET_ACCESS_KEY
110
- });
111
- }
112
- return this.s3Client;
113
- }
114
-
115
- private base64Client? : Base64Client;
116
- get Base64Client(): Base64Client {
117
- if (this.base64Client === undefined) {
118
- this.base64Client = new Base64Client();
119
- }
120
- return this.base64Client;
121
- }
122
- }
@@ -1,34 +0,0 @@
1
- const cron = require('node-cron');
2
- import fs from 'fs';
3
- import path from 'path';
4
- import { BaseCron } from "./BaseCron";
5
-
6
- export const runCron = async (dir: string, logCallback?: (ex: Error) => Promise<void>) => {
7
- const files = fs.readdirSync(dir);
8
- for (let file of files) {
9
- if (['BaseCron.ts'].includes(file)) {
10
- continue;
11
- }
12
-
13
- const filePath = path.join(dir, file);
14
- const module = await import(filePath);
15
- const cronClass = module.default;
16
- const cronInstance: BaseCron = new cronClass();
17
-
18
- cron.schedule(cronInstance.CronSchedule, async () => {
19
- try {
20
- await cronInstance.setUp();
21
- await cronInstance.run();
22
- } catch (ex: unknown) {
23
- if (logCallback !== undefined && ex instanceof Error) {
24
- await logCallback(ex).
25
- catch(() => {
26
- // ログ送信時にエラーになっても握りつぶす
27
- });
28
- }
29
- } finally {
30
- await cronInstance.tearDown();
31
- }
32
- });
33
- }
34
- }
@@ -1,25 +0,0 @@
1
- export type DayType =
2
- 1 | 2 | 3 | 4 | 5 | 6 | 7
3
-
4
- export type MonthType =
5
- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
6
- 10 | 11 | 12
7
-
8
- export type DateType =
9
- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10
- 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
11
- 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
12
- 30 | 31
13
-
14
- export type HourType =
15
- 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
16
- 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
17
- 20 | 21 | 22 | 23
18
-
19
- export type MinuteSecondType =
20
- 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
21
- 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
22
- 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
23
- 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
24
- 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
25
- 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59