From 902939ddfc371ee6676fc9e34b03882baece7370 Mon Sep 17 00:00:00 2001 From: speak-mentaiko <1717miyawaki@gmail.com> Date: Tue, 17 Dec 2024 21:33:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BA=88=E9=81=B8=E3=82=92=E5=90=8C?= =?UTF-8?q?=E6=99=82=E3=81=AB=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/kcms/src/match/service/generatePre.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/kcms/src/match/service/generatePre.ts b/packages/kcms/src/match/service/generatePre.ts index b3855e49..c74aec4a 100644 --- a/packages/kcms/src/match/service/generatePre.ts +++ b/packages/kcms/src/match/service/generatePre.ts @@ -14,11 +14,18 @@ export class GeneratePreMatchService { ) {} async handle(departmentType: DepartmentType): Promise> { - if (!config.match.pre.course[departmentType]) { + if (!config.match.pre.course[departmentType].length) { return Result.err(new Error('DepartmentType is not defined')); } - const pair = await this.makePairs(departmentType); - return await this.makeMatches(pair); + const matches = new Map(); + for (const e of config.departmentTypes) { + if (config.match.pre.course[e].length) { + const pair = await this.makePairs(e); + const match = await this.makeMatches(pair); + matches.set(e, match); + } + } + return matches.get(departmentType); } private async makeMatches( From afc94e327ba52b2e7b1f8392bfee4214e68b92b2 Mon Sep 17 00:00:00 2001 From: speak-mentaiko <1717miyawaki@gmail.com> Date: Wed, 8 Jan 2025 13:56:13 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E3=81=99=E3=81=B9=E3=81=A6?= =?UTF-8?q?=E3=81=AE=E4=BA=88=E9=81=B8=E3=82=92=E5=90=8C=E6=99=82=E3=81=AB?= =?UTF-8?q?=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/match/adaptor/controller/match.ts | 35 ++++- .../kcms/src/match/adaptor/validator/match.ts | 7 + packages/kcms/src/match/main.ts | 19 +++ packages/kcms/src/match/routing.ts | 24 +++ .../kcms/src/match/service/generateAllPre.ts | 138 ++++++++++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 packages/kcms/src/match/service/generateAllPre.ts diff --git a/packages/kcms/src/match/adaptor/controller/match.ts b/packages/kcms/src/match/adaptor/controller/match.ts index 5cd92bca..e259d59a 100644 --- a/packages/kcms/src/match/adaptor/controller/match.ts +++ b/packages/kcms/src/match/adaptor/controller/match.ts @@ -1,11 +1,12 @@ import { z } from '@hono/zod-openapi'; import { Result } from '@mikuroxina/mini-fn'; -import { DepartmentType, MatchType } from 'config'; +import { config, DepartmentType, MatchType } from 'config'; import { Team, TeamID } from '../../../team/models/team'; import { FetchTeamService } from '../../../team/service/fetchTeam'; import { MainMatch, MainMatchID } from '../../model/main'; import { PreMatch, PreMatchID } from '../../model/pre'; import { FetchRunResultService } from '../../service/fetchRunResult'; +import { GenerateAllPreMatchService } from '../../service/generateAllPre'; import { GenerateMainMatchService } from '../../service/generateMain'; import { GeneratePreMatchService } from '../../service/generatePre'; import { GenerateRankingService } from '../../service/generateRanking'; @@ -19,6 +20,7 @@ import { MainSchema, PostMatchGenerateManualResponseSchema, PostMatchGenerateResponseSchema, + PostPreMatchGenerateResponseSchema, PreSchema, RunResultSchema, ShortMainSchema, @@ -30,6 +32,7 @@ export class MatchController { private readonly getMatchService: GetMatchService, private readonly fetchTeamService: FetchTeamService, private readonly generatePreMatchService: GeneratePreMatchService, + private readonly generateAllPreMatchService: GenerateAllPreMatchService, private readonly generateRankingService: GenerateRankingService, private readonly fetchRunResultService: FetchRunResultService, private readonly generateMainMatchService: GenerateMainMatchService @@ -123,6 +126,36 @@ export class MatchController { ); } + async generateAllPreMatch(): Promise< + Result.Result> + > { + const data = []; + const res = await this.generateAllPreMatchService.handle(); + for (const e of config.departmentTypes) { + const matches = res.get(e); + if (matches) { + if (Result.isErr(matches)) return matches; + const createMatches = Result.unwrap(matches).map((v): z.infer => { + return { + id: v.getID(), + matchCode: `${v.getCourseIndex()}-${v.getMatchIndex()}`, + matchType: 'pre', + departmentType: v.getDepartmentType(), + leftTeamID: v.getTeamID1(), + rightTeamID: v.getTeamID2(), + runResults: [], + }; + }); + data.push({ + departmentType: e, + matches: createMatches, + }); + } + } + + return Result.ok(data); + } + async generateMatchManual( departmentType: DepartmentType, team1ID: string, diff --git a/packages/kcms/src/match/adaptor/validator/match.ts b/packages/kcms/src/match/adaptor/validator/match.ts index 222435f5..0371674a 100644 --- a/packages/kcms/src/match/adaptor/validator/match.ts +++ b/packages/kcms/src/match/adaptor/validator/match.ts @@ -116,6 +116,13 @@ export const PostMatchGenerateResponseSchema = z.union([ ShortMainSchema.array(), ]); +export const PostPreMatchGenerateResponseSchema = z.array( + z.object({ + departmentType: DepartmentTypeSchema, + matches: ShortPreSchema.array(), + }) +); + export const PostMatchGenerateManualParamsSchema = z.object({ departmentType: DepartmentTypeSchema, }); diff --git a/packages/kcms/src/match/main.ts b/packages/kcms/src/match/main.ts index c35a7aa3..1cb2bfaa 100644 --- a/packages/kcms/src/match/main.ts +++ b/packages/kcms/src/match/main.ts @@ -23,9 +23,11 @@ import { PostMatchGenerateManualRoute, PostMatchGenerateRoute, PostMatchRunResultRoute, + PostPreMatchGenerateRoute, } from './routing'; import { CreateRunResultService } from './service/createRunResult'; import { FetchRunResultService } from './service/fetchRunResult'; +import { GenerateAllPreMatchService } from './service/generateAllPre'; import { GenerateMainMatchService } from './service/generateMain'; import { GeneratePreMatchService } from './service/generatePre'; import { GenerateRankingService } from './service/generateRanking'; @@ -54,6 +56,13 @@ const generatePreMatchService = new GeneratePreMatchService( idGenerator, preMatchRepository ); + +const generateAllPreMatchService = new GenerateAllPreMatchService( + fetchTeamService, + idGenerator, + preMatchRepository +); + const generateRankingService = new GenerateRankingService(preMatchRepository, mainMatchRepository); const fetchRunResultService = new FetchRunResultService(mainMatchRepository, preMatchRepository); const generateMainMatchService = new GenerateMainMatchService(mainMatchRepository, idGenerator); @@ -61,6 +70,7 @@ const matchController = new MatchController( getMatchService, fetchTeamService, generatePreMatchService, + generateAllPreMatchService, generateRankingService, fetchRunResultService, generateMainMatchService @@ -108,6 +118,15 @@ matchHandler.openapi(PostMatchGenerateRoute, async (c) => { return c.json(res[1], 200); }); +matchHandler.openapi(PostPreMatchGenerateRoute, async (c) => { + const res = await matchController.generateAllPreMatch(); + if (Result.isErr(res)) { + return c.json({ description: res[1].message }, 400); + } + + return c.json(res[1], 200); +}); + /** * 本戦試合を手動で生成 */ diff --git a/packages/kcms/src/match/routing.ts b/packages/kcms/src/match/routing.ts index 006bb026..db33a233 100644 --- a/packages/kcms/src/match/routing.ts +++ b/packages/kcms/src/match/routing.ts @@ -17,6 +17,7 @@ import { PostMatchGenerateResponseSchema, PostMatchRunResultParamsSchema, PostMatchRunResultRequestSchema, + PostPreMatchGenerateResponseSchema, } from '../match/adaptor/validator/match'; export const GetMatchRoute = createRoute({ @@ -138,6 +139,29 @@ export const PostMatchGenerateRoute = createRoute({ }, }); +export const PostPreMatchGenerateRoute = createRoute({ + method: 'post', + path: '/match/pre', + responses: { + 200: { + content: { + 'application/json': { + schema: PostPreMatchGenerateResponseSchema, + }, + }, + description: 'Generate match table', + }, + 400: { + content: { + 'application/json': { + schema: CommonErrorSchema, + }, + }, + description: 'Common error', + }, + }, +}); + export const PostMatchGenerateManualRoute = createRoute({ method: 'post', path: '/match/main/{departmentType}/generate/manual', diff --git a/packages/kcms/src/match/service/generateAllPre.ts b/packages/kcms/src/match/service/generateAllPre.ts new file mode 100644 index 00000000..76b35eb4 --- /dev/null +++ b/packages/kcms/src/match/service/generateAllPre.ts @@ -0,0 +1,138 @@ +import { Result } from '@mikuroxina/mini-fn'; +import { config, DepartmentType } from 'config'; +import { SnowflakeIDGenerator } from '../../id/main'; +import { Team } from '../../team/models/team'; +import { FetchTeamService } from '../../team/service/fetchTeam'; +import { PreMatch } from '../model/pre'; +import { PreMatchRepository } from '../model/repository'; + +export class GenerateAllPreMatchService { + constructor( + private readonly fetchTeam: FetchTeamService, + private readonly idGenerator: SnowflakeIDGenerator, + private readonly preMatchRepository: PreMatchRepository + ) {} + + async handle(): Promise>> { + const matches = new Map>(); + for (const e of config.departmentTypes) { + if (config.match.pre.course[e].length) { + const pair = await this.makePairs(e); + const match = await this.makeMatches(pair); + matches.set(e, match); + } + } + return matches; + } + + private async makeMatches( + data: (Team | undefined)[][][] + ): Promise> { + // 与えられたペアをもとに試合を生成する + + // コースごとに生成 + const generated = data.map((course, courseIndex) => { + // ペアをもとに試合を生成 + return course.map((pair, matchIndex): Result.Result => { + const id = this.idGenerator.generate(); + if (Result.isErr(id)) { + return id; + } + const match = PreMatch.new({ + id: Result.unwrap(id), + // ToDo: 他部門のコースがすでに使用されているときにコース番号をどうするかを考える + courseIndex: courseIndex + 1, + matchIndex: matchIndex + 1, + departmentType: (pair[0] || pair[1]!).getDepartmentType(), + teamID1: pair[0]?.getID(), + teamID2: pair[1]?.getID(), + runResults: [], + }); + return Result.ok(match); + }); + }); + const flatten = generated.flat(); + const match = flatten.filter((v) => Result.isOk(v)).map((v) => Result.unwrap(v)); + + const res = await this.preMatchRepository.createBulk(match); + if (Result.isErr(res)) { + return res; + } + + return Result.ok(match); + } + + /** + * チームのペアだけを作る関数 + */ + async makePairs(departmentType: DepartmentType): Promise<(Team | undefined)[][][]> { + // 多言語環境でソート可能にするためにcollatorを使う + const collator = new Intl.Collator('ja'); + const courseCount = config.match.pre.course[departmentType].length; + + // エントリー済みのチームを取得 + const teamRes = await this.fetchTeam.findAll(); + if (Result.isErr(teamRes)) { + return []; + } + const team = Result.unwrap(teamRes).filter( + (v) => v.getIsEntered() && v.getDepartmentType() === departmentType + ); + + // チームをクラブ名でソートする (ToDo: クラブ名がない場合にどこの位置に動かすかを決める必要がありそう + const teams = team.sort((a, b) => + collator.compare(a.getClubName() ?? 'N', b.getClubName() ?? 'N') + ); + + // コースの数でスライスする + // 初期化時に必要な個数作っておく + const slicedTeams: Team[][] = new Array( + Math.ceil(teams.length / config.match.pre.course[departmentType].length) + ).fill([]); + for (let i = 0; i < Math.ceil(teams.length / courseCount); i++) { + // コース数 + slicedTeams[i] = teams.slice(i * courseCount, (i + 1) * courseCount); + } + + /* スライスされた配列を転置する + * 列数 != 行数の場合、undefinedが入るので、filterで除外している + */ + const transpose = slicedTeams[0] + .map((_, i) => slicedTeams.map((r) => r[i])) + .map((r) => r.filter((v) => v !== undefined)); + + // 配列の要素ごとに、右側のコースを走る相手を決める + return transpose.map((v) => { + if (v.length === 1) { + console.warn('WANING: 1チームだけのコースがあります'); + return [ + [v[0], undefined], + [undefined, undefined], + [undefined, v[0]], + ]; + } else if (v.length === 2) { + console.warn('WANING: 2チームだけのコースがあります'); + return [ + [v[0], v[1]], + [undefined, undefined], + [v[1], v[0]], + ]; + } else if (v.length === 3) { + console.warn('WANING: 3チームだけのコースがあります'); + return [ + [v[0], v[2]], + [v[1], undefined], + [undefined, v[0]], + [v[2], v[1]], + ]; + } else { + // ずらす数(floor(配列の長さ/2)) + const shift = Math.floor(v.length / 2); + // ずらした配列を作成 + const shifted = v.slice(shift).concat(v.slice(0, shift)); + // ペアを作る + return v.map((_, i) => [v[i], shifted[i]]); + } + }); + } +} From cf425483135741ca34bf7c959d7a349bffd9f926 Mon Sep 17 00:00:00 2001 From: speak-mentaiko <1717miyawaki@gmail.com> Date: Wed, 8 Jan 2025 23:27:13 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/match/adaptor/controller/match.ts | 4 +- .../src/match/service/generateAllPre.test.ts | 63 +++++++++++++++++++ .../kcms/src/match/service/generatePre.ts | 13 +--- 3 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 packages/kcms/src/match/service/generateAllPre.test.ts diff --git a/packages/kcms/src/match/adaptor/controller/match.ts b/packages/kcms/src/match/adaptor/controller/match.ts index e259d59a..9f73e353 100644 --- a/packages/kcms/src/match/adaptor/controller/match.ts +++ b/packages/kcms/src/match/adaptor/controller/match.ts @@ -1,6 +1,6 @@ import { z } from '@hono/zod-openapi'; import { Result } from '@mikuroxina/mini-fn'; -import { config, DepartmentType, MatchType } from 'config'; +import { DepartmentType, MatchType } from 'config'; import { Team, TeamID } from '../../../team/models/team'; import { FetchTeamService } from '../../../team/service/fetchTeam'; import { MainMatch, MainMatchID } from '../../model/main'; @@ -131,7 +131,7 @@ export class MatchController { > { const data = []; const res = await this.generateAllPreMatchService.handle(); - for (const e of config.departmentTypes) { + for (const e of res.keys()) { const matches = res.get(e); if (matches) { if (Result.isErr(matches)) return matches; diff --git a/packages/kcms/src/match/service/generateAllPre.test.ts b/packages/kcms/src/match/service/generateAllPre.test.ts new file mode 100644 index 00000000..3ade436c --- /dev/null +++ b/packages/kcms/src/match/service/generateAllPre.test.ts @@ -0,0 +1,63 @@ +import { Result } from '@mikuroxina/mini-fn'; +import { config } from 'config'; +import { describe, expect, it } from 'vitest'; +import { SnowflakeIDGenerator } from '../../id/main'; +import { DummyRepository } from '../../team/adaptor/repository/dummyRepository'; +import { TeamID } from '../../team/models/team'; +import { FetchTeamService } from '../../team/service/fetchTeam'; +import { testTeamData } from '../../testData/entry'; +import { DummyPreMatchRepository } from '../adaptor/dummy/preMatchRepository'; +import { GenerateAllPreMatchService } from './generateAllPre'; + +describe('GeneratePreMatchService', () => { + const teamRepository = new DummyRepository([...testTeamData.values()]); + const fetchService = new FetchTeamService(teamRepository); + const generator = new SnowflakeIDGenerator(1, () => + BigInt(new Date('2024/01/01 00:00:00 UTC').getTime()) + ); + const preMatchRepository = new DummyPreMatchRepository(); + const generateService = new GenerateAllPreMatchService( + fetchService, + generator, + preMatchRepository + ); + + const expectedTeamPair = [ + [ + ['A1', 'B3'], + ['A4', 'N1'], + ['B3', 'A1'], + ['N1', 'A4'], + ], + [ + ['A2', 'C1'], + ['B1', 'N2'], + ['C1', 'A2'], + ['N2', 'B1'], + ], + [ + ['A3', 'C2'], + ['B2', undefined], + [undefined, 'A3'], + ['C2', 'B2'], + ], + ]; + + it('正しく予選対戦表を生成できる', async () => { + const res = await generateService.handle(); + for (const e of res.keys()) { + const matches = res.get(e); + if (matches) { + expect(Result.isOk(matches)).toBe(true); + for (let i = 0; i < config.match.pre.course[e].length; i++) { + const course = Result.unwrap(matches).filter((v) => v.getCourseIndex() === i + 1); + const pair = course.map((v) => [ + testTeamData.get(v.getTeamID1() ?? ('' as TeamID))?.getTeamName(), + testTeamData.get(v.getTeamID2() ?? ('' as TeamID))?.getTeamName(), + ]); + expect(pair).toStrictEqual(expectedTeamPair[i]); + } + } + } + }); +}); diff --git a/packages/kcms/src/match/service/generatePre.ts b/packages/kcms/src/match/service/generatePre.ts index 62079abd..40b180a9 100644 --- a/packages/kcms/src/match/service/generatePre.ts +++ b/packages/kcms/src/match/service/generatePre.ts @@ -14,18 +14,11 @@ export class GeneratePreMatchService { ) {} async handle(departmentType: DepartmentType): Promise> { - if (!config.match.pre.course[departmentType].length) { + if (!config.match.pre.course[departmentType]) { return Result.err(new Error('DepartmentType is not defined')); } - const matches = new Map(); - for (const e of config.departmentTypes) { - if (config.match.pre.course[e].length) { - const pair = await this.makePairs(e); - const match = await this.makeMatches(pair); - matches.set(e, match); - } - } - return matches.get(departmentType); + const pair = await this.makePairs(departmentType); + return await this.makeMatches(pair); } private async makeMatches(