Skip to content

Commit 57516fd

Browse files
committed
Merge branch 'release/1.0.0'
2 parents f7f9302 + 6672635 commit 57516fd

8 files changed

Lines changed: 1802 additions & 1425 deletions

File tree

.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ module.exports = {
3737
'@stylistic/no-trailing-spaces': 'error',
3838
'@stylistic/no-multi-spaces': 'error',
3939
},
40+
overrides: [
41+
{
42+
files: ['**/*.spec.ts', '**/*.test.ts'],
43+
rules: {
44+
'@typescript-eslint/no-explicit-any': 'off'
45+
}
46+
}
47+
]
4048
};

jest.config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { pathsToModuleNameMapper } from 'ts-jest';
2+
import { compilerOptions } from './tsconfig.json';
3+
4+
export default {
5+
preset: 'ts-jest',
6+
testEnvironment: 'node',
7+
moduleFileExtensions: [ 'ts', 'tsx', 'js', 'jsx', 'json', 'node' ],
8+
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '<rootDir>/' }),
9+
testMatch: [ '**/*.spec.ts' ],
10+
collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/main.ts', '!src/**/*.module.ts' ],
11+
coverageDirectory: 'coverage',
12+
clearMocks: true
13+
};

package.json

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "aeum-gil-api",
3-
"version": "0.21.0",
3+
"version": "1.0.0",
44
"description": "",
55
"author": "",
66
"private": true,
@@ -10,7 +10,6 @@
1010
"start": "nest start",
1111
"start:dev": "yarn nest start --watch",
1212
"start:debug": "nest start --debug --watch",
13-
"start:prod": "node dist/main",
1413
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
1514
"test": "yarn jest",
1615
"test:watch": "jest --watch",
@@ -19,67 +18,51 @@
1918
"test:e2e": "jest --config ./test/jest-e2e.json"
2019
},
2120
"dependencies": {
22-
"@nestjs/common": "^10.0.0",
23-
"@nestjs/config": "^3.3.0",
24-
"@nestjs/core": "^10.0.0",
21+
"@nestjs/common": "^11.0.12",
22+
"@nestjs/config": "^4.0.1",
23+
"@nestjs/core": "^11.0.12",
2524
"@nestjs/jwt": "^11.0.0",
2625
"@nestjs/passport": "^11.0.5",
27-
"@nestjs/platform-express": "^10.0.0",
28-
"@nestjs/typeorm": "^10.0.2",
26+
"@nestjs/platform-express": "^11.0.12",
27+
"@nestjs/typeorm": "^11.0.0",
2928
"argon2": "^0.41.1",
3029
"aws-sdk": "^2.1692.0",
31-
"axios": "^1.7.9",
30+
"axios": "^1.8.4",
3231
"class-transformer": "^0.5.1",
3332
"class-validator": "^0.14.1",
3433
"dayjs": "^1.11.13",
3534
"lodash": "^4.17.21",
36-
"mysql2": "^3.11.3",
35+
"mysql2": "^3.14.0",
3736
"passport": "^0.7.0",
3837
"passport-jwt": "^4.0.1",
39-
"reflect-metadata": "^0.2.0",
40-
"rxjs": "^7.8.1",
41-
"typeorm": "^0.3.20",
38+
"reflect-metadata": "^0.2.2",
39+
"rxjs": "^7.8.2",
40+
"typeorm": "^0.3.21",
4241
"typeorm-transactional": "^0.5.0"
4342
},
4443
"devDependencies": {
45-
"@nestjs/cli": "^10.4.5",
46-
"@nestjs/schematics": "^10.0.0",
47-
"@nestjs/swagger": "^8.0.7",
48-
"@nestjs/testing": "^10.0.0",
49-
"@stylistic/eslint-plugin": "^2.9.0",
50-
"@swc/cli": "^0.5.0",
51-
"@swc/core": "^1.8.0",
52-
"@types/express": "^4.17.17",
53-
"@types/jest": "^29.5.2",
44+
"@nestjs/cli": "^11.0.5",
45+
"@nestjs/schematics": "^11.0.2",
46+
"@nestjs/swagger": "^11.1.0",
47+
"@nestjs/testing": "^11.0.12",
48+
"@stylistic/eslint-plugin": "^4.2.0",
49+
"@swc/cli": "^0.6.0",
50+
"@swc/core": "^1.11.13",
51+
"@types/express": "^5.0.1",
52+
"@types/jest": "^29.5.14",
5453
"@types/lodash": "^4.17.16",
55-
"@types/node": "^20.3.1",
54+
"@types/node": "^22.13.13",
5655
"@types/passport-jwt": "^4.0.1",
57-
"@typescript-eslint/eslint-plugin": "^8.0.0",
58-
"@typescript-eslint/parser": "^8.0.0",
59-
"eslint": "^8.42.0",
60-
"jest": "^29.5.0",
56+
"@typescript-eslint/eslint-plugin": "^8.28.0",
57+
"@typescript-eslint/parser": "^8.28.0",
58+
"eslint": "^9.23.0",
59+
"jest": "^29.7.0",
60+
"jest-mock-extended": "^4.0.0-beta1",
6161
"source-map-support": "^0.5.21",
62-
"ts-jest": "^29.1.0",
63-
"ts-loader": "^9.4.3",
64-
"ts-node": "^10.9.1",
62+
"ts-jest": "^29.3.0",
63+
"ts-loader": "^9.5.2",
64+
"ts-node": "^10.9.2",
6565
"tsconfig-paths": "^4.2.0",
66-
"typescript": "^5.1.3"
67-
},
68-
"jest": {
69-
"moduleFileExtensions": [
70-
"js",
71-
"json",
72-
"ts"
73-
],
74-
"rootDir": "src",
75-
"testRegex": ".*\\.spec\\.ts$",
76-
"transform": {
77-
"^.+\\.(t|j)s$": "ts-jest"
78-
},
79-
"collectCoverageFrom": [
80-
"**/*.(t|j)s"
81-
],
82-
"coverageDirectory": "../coverage",
83-
"testEnvironment": "node"
66+
"typescript": "^5.8.2"
8467
}
8568
}

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ async function bootstrap() {
1717

1818
const swaggerConfig = new DocumentBuilder()
1919
.setTitle('에움길 API')
20-
.setVersion('0.21.0')
20+
.setVersion('1.0.0')
2121
.addBearerAuth()
2222
.build();
2323
const document = SwaggerModule.createDocument(app, swaggerConfig, {
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { Test } from '@nestjs/testing';
2+
import { mock } from 'jest-mock-extended';
3+
import { ItemActionType } from 'src/database/entity';
4+
import { ChapterRepository, ChoiceOptionRepository, EndingRecordRepository, EndingRepository, ItemRepository, PageRepository, PlayRecordRepository, PlayStatusRepository } from 'src/database/repository';
5+
import { ArrayUtil } from 'src/util/array-util.service';
6+
import { GameService } from './game.service';
7+
8+
describe('GameService', () => {
9+
let gameService: GameService;
10+
11+
const mockChapterRepository = mock<ChapterRepository>();
12+
const mockPageRepository = mock<PageRepository>();
13+
const mockItemRepository = mock<ItemRepository>();
14+
const mockPlayRecordRepository = mock<PlayRecordRepository>();
15+
const mockEndingRepository = mock<EndingRepository>();
16+
const mockEndingRecordRepository = mock<EndingRecordRepository>();
17+
const mockPlayStatusRepository = mock<PlayStatusRepository>();
18+
const mockChoiceOptionRepository = mock<ChoiceOptionRepository>();
19+
const arrayUtil = new ArrayUtil();
20+
21+
beforeEach(async () => {
22+
const module = await Test.createTestingModule({
23+
providers: [
24+
GameService,
25+
{ provide: ChapterRepository, useValue: mockChapterRepository },
26+
{ provide: PageRepository, useValue: mockPageRepository },
27+
{ provide: ItemRepository, useValue: mockItemRepository },
28+
{ provide: PlayRecordRepository, useValue: mockPlayRecordRepository },
29+
{ provide: EndingRepository, useValue: mockEndingRepository },
30+
{ provide: EndingRecordRepository, useValue: mockEndingRecordRepository },
31+
{ provide: PlayStatusRepository, useValue: mockPlayStatusRepository },
32+
{ provide: ChoiceOptionRepository, useValue: mockChoiceOptionRepository },
33+
{ provide: ArrayUtil, useValue: arrayUtil }
34+
]
35+
}).compile();
36+
37+
gameService = module.get<GameService>(GameService);
38+
});
39+
40+
describe('Test calculateItems', () => {
41+
test('소유 O, 랜덤 획득 O', () => {
42+
const ownedItems = [
43+
{ itemId: 1, count: 1 }
44+
];
45+
const itemMappings = [ {
46+
choiceOptionId: 1,
47+
itemId: 1,
48+
actionType: ItemActionType.RandomGain,
49+
createdAt: new Date(),
50+
updatedAt: new Date()
51+
} ];
52+
53+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
54+
expect(result).toStrictEqual([ { itemId: 1, count: 2 } ]);
55+
});
56+
57+
test('소유 O, 매핑 O, 획득', () => {
58+
const ownedItems = [
59+
{ itemId: 1, count: 1 }
60+
];
61+
const itemMappings = [ {
62+
choiceOptionId: 1,
63+
itemId: 1,
64+
actionType: ItemActionType.Gain,
65+
createdAt: new Date(),
66+
updatedAt: new Date()
67+
} ];
68+
69+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
70+
expect(result).toStrictEqual([ { itemId: 1, count: 2 } ]);
71+
});
72+
73+
test('소유 O, 매핑 O, 소모 - 아이템이 없으면 null', () => {
74+
const ownedItems = [
75+
{ itemId: 1, count: 1 }
76+
];
77+
const itemMappings = [ {
78+
choiceOptionId: 1,
79+
itemId: 1,
80+
actionType: ItemActionType.Loss,
81+
createdAt: new Date(),
82+
updatedAt: new Date()
83+
} ];
84+
85+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
86+
expect(result).toBeNull();
87+
});
88+
89+
test('소유 O, 매핑 O, 소모 - 다른 아이템 있는 경우', () => {
90+
const ownedItems = [
91+
{ itemId: 1, count: 1 },
92+
{ itemId: 2, count: 1 }
93+
];
94+
const itemMappings = [ {
95+
choiceOptionId: 1,
96+
itemId: 1,
97+
actionType: ItemActionType.Loss,
98+
createdAt: new Date(),
99+
updatedAt: new Date()
100+
} ];
101+
102+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
103+
expect(result).toStrictEqual([ { itemId: 2, count: 1 } ]);
104+
});
105+
106+
test('소유 O, 매핑 X', () => {
107+
const ownedItems = [
108+
{ itemId: 1, count: 1 }
109+
];
110+
111+
const result = (gameService as any).calculateItems(ownedItems);
112+
expect(result).toStrictEqual(ownedItems);
113+
});
114+
115+
test('소유 X, 랜덤 획득 O', () => {
116+
const ownedItems = null;
117+
const itemMappings = [ {
118+
choiceOptionId: 1,
119+
itemId: 1,
120+
actionType: ItemActionType.RandomGain,
121+
createdAt: new Date(),
122+
updatedAt: new Date()
123+
} ];
124+
125+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
126+
expect(result).toStrictEqual([ { itemId: 1, count: 1 } ]);
127+
});
128+
129+
test('소유 X, 매핑 O - 아이템 획득인 경우', () => {
130+
const ownedItems = null;
131+
const itemMappings = [ {
132+
choiceOptionId: 1,
133+
itemId: 1,
134+
actionType: ItemActionType.Gain,
135+
createdAt: new Date(),
136+
updatedAt: new Date()
137+
} ];
138+
139+
const result = (gameService as any).calculateItems(ownedItems, itemMappings);
140+
expect(result).toStrictEqual([ { itemId: 1, count: 1 } ]);
141+
});
142+
143+
test('소유 X, 매핑 O - 아이템 소모인 경우', () => {
144+
const ownedItems = null;
145+
const itemMappings = [ {
146+
choiceOptionId: 1,
147+
itemId: 1,
148+
actionType: ItemActionType.Loss,
149+
createdAt: new Date(),
150+
updatedAt: new Date()
151+
} ];
152+
153+
expect(() => {
154+
(gameService as any).calculateItems(ownedItems, itemMappings);
155+
}).toThrow();
156+
});
157+
});
158+
});

src/module/game/game.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ export class GameService {
9898
return { moveTargetType, targetId, ownedItems: calculatedItems };
9999
}
100100

101+
/**
102+
* 아이템은 무조건 1개씩 얻거나 소모한다.
103+
* 아이템을 모두 소모한 경우 소유 목록에서 제거한다.
104+
*/
101105
private calculateItems(ownedItems: OwnedItem[] | null, itemMappings?: ChoiceOptionItemMapping[]) {
102106
if (!itemMappings?.length) {
103107
return ownedItems;
@@ -112,7 +116,7 @@ export class GameService {
112116
const itemSet = new Set([ ...ownedItemIds, ...mappingItemIds ]);
113117
const itemIds = Array.from(itemSet);
114118

115-
return itemIds.map((itemId) => {
119+
const calculated = itemIds.map((itemId) => {
116120
const ownedItem = ownedItems?.find((oi) => oi.itemId === itemId);
117121
const mappingItem = calculateTargetItems.find((im) => im.itemId === itemId);
118122

@@ -128,8 +132,6 @@ export class GameService {
128132
if (mappingItem) {
129133
const { actionType } = mappingItem;
130134

131-
// TODO: 아이템 소모일 때 개수가 더 적으면 throw Error
132-
133135
const calculatedCount = actionType === ItemActionType.Gain ? count + 1 : count - 1;
134136

135137
// 전부 소모했으면 제거
@@ -160,6 +162,8 @@ export class GameService {
160162
return { itemId, count: 1 };
161163
}
162164
}).filter(isExists);
165+
166+
return calculated.length ? calculated : null;
163167
}
164168

165169
@Transactional()

tsconfig.json

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,19 @@
33
"module": "commonjs",
44
"target": "ES2021",
55
"baseUrl": "./",
6+
"paths": {
7+
"src/*": [ "src/*" ],
8+
"src/database/entity": [ "src/database/entity/index.ts" ],
9+
"src/database/repository": [ "src/database/repository/index.ts" ]
10+
},
611
"outDir": "./dist",
712
"experimentalDecorators": true,
813
"removeComments": true,
914
"noImplicitAny": true,
1015
"emitDecoratorMetadata": true,
1116
"strict": true,
1217
"strictPropertyInitialization": false,
13-
"esModuleInterop": true
14-
// "allowSyntheticDefaultImports": true,
15-
// "sourceMap": true,
16-
// "declaration": true,
17-
// "incremental": true,
18-
// "skipLibCheck": true,
19-
// "strictNullChecks": false,
20-
// "strictBindCallApply": false,
21-
// "forceConsistentCasingInFileNames": false,
22-
// "noFallthroughCasesInSwitch": false
18+
"esModuleInterop": true,
19+
"resolveJsonModule": true
2320
}
2421
}

0 commit comments

Comments
 (0)