feat: update typecheck script for incremental compilation and add JosaUtil for Korean postposition handling

This commit is contained in:
2025-12-29 12:50:45 +00:00
parent cd43445e4b
commit 30de11797b
5 changed files with 143 additions and 5 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"dev": "tsdown -c ../../tsdown.config.ts -F @sammo-ts/common --watch",
"lint": "node -e \"console.log('lint not configured')\"",
"test": "vitest run --config vitest.config.ts",
"typecheck": "tsc --noEmit"
"typecheck": "tsc -b"
},
"dependencies": {
"js-sha512": "^0.9.0"
+1
View File
@@ -4,6 +4,7 @@ export * from './util/BytesLike.js';
export * from './util/convertBytesLikeToArrayBuffer.js';
export * from './util/convertBytesLikeToUint8Array.js';
export * from './util/LiteHashDRBG.js';
export * from './util/JosaUtil.js';
export * from './util/RNG.js';
export * from './util/RandUtil.js';
export * from './util/TestRNG.js';
+132
View File
@@ -0,0 +1,132 @@
export type JosaKey =
| '은'
| '이'
| '과'
| '이나'
| '을'
| '으로'
| '이라'
| '이랑';
const DEFAULT_POSTPOSITION: Record<JosaKey, string> = {
: '는',
: '가',
: '와',
: '나',
: '를',
: '로',
: '라',
: '랑',
};
const buildMapPostPosition = (): Record<string, JosaKey> => {
const map: Record<string, JosaKey> = {};
for (const [key, value] of Object.entries(DEFAULT_POSTPOSITION)) {
const k = key as JosaKey;
map[key] = k;
map[value] = k;
map[`(${key})${value}`] = k;
}
return map;
};
const MAP_POSTPOSITION = buildMapPostPosition();
const REG_INVALID_CHAR = /[^a-zA-Z0-9---\s]+/gu;
const REG_TARGET_CHAR = /^[\s\S]*?(\S*)\s*$/u;
const KO_START_CODE = 0xac00;
const KO_FINISH_CODE = 0xd7a3;
const JONGSUNG_RIEUL = 8;
const getLastChar = (text: string): string => {
const cleaned = text
.replace(REG_INVALID_CHAR, ' ')
.replace(REG_TARGET_CHAR, '$1')
.trim();
if (!cleaned) {
return '';
}
const chars = Array.from(cleaned);
return chars[chars.length - 1] ?? '';
};
const getDigitJongsung = (
digit: number
): { has: boolean; rieul: boolean } => {
switch (digit) {
case 0:
case 3:
case 6:
return { has: true, rieul: false };
case 1:
case 7:
case 8:
return { has: true, rieul: true };
default:
return { has: false, rieul: false };
}
};
const hasJongsung = (text: string, isRo: boolean): boolean => {
const lastChar = getLastChar(text);
if (!lastChar) {
return false;
}
const code = lastChar.codePointAt(0);
if (code === undefined) {
return false;
}
if (code >= KO_START_CODE && code <= KO_FINISH_CODE) {
const jongsung = (code - KO_START_CODE) % 28;
if (jongsung === 0) {
return false;
}
if (isRo && jongsung === JONGSUNG_RIEUL) {
return false;
}
return true;
}
if (lastChar >= 'ㄱ' && lastChar <= 'ㅎ') {
if (isRo && lastChar === 'ㄹ') {
return false;
}
return true;
}
if (lastChar >= '0' && lastChar <= '9') {
const { has, rieul } = getDigitJongsung(Number(lastChar));
if (isRo && rieul) {
return false;
}
return has;
}
const lower = lastChar.toLowerCase();
const isVowel = ['a', 'e', 'i', 'o', 'u', 'y'].includes(lower);
return !isVowel;
};
export class JosaUtil {
static pick(text: string | null | undefined, wJongsung: string, woJongsung = ''): string {
const normalizedText = text ?? '';
let withJongsung = wJongsung;
let withoutJongsung = woJongsung;
if (!withoutJongsung) {
const mapped = MAP_POSTPOSITION[wJongsung];
if (!mapped) {
throw new Error('올바르지 않은 조사 지정');
}
withJongsung = mapped;
withoutJongsung = DEFAULT_POSTPOSITION[mapped];
}
const isRo = withJongsung === '으로';
return hasJongsung(normalizedText, isRo)
? withJongsung
: withoutJongsung;
}
static put(text: string, wJongsung: string, woJongsung = ''): string {
return text + JosaUtil.pick(text, wJongsung, woJongsung);
}
}
@@ -27,6 +27,7 @@ import type {
} from '../../engine.js';
import { createGeneralPatchEffect, createLogEffect } from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
export interface AssignmentArgs {
destGeneralId: number;
@@ -103,6 +104,8 @@ export class ActionResolver<
const cityName = this.env.formatCityName
? this.env.formatCityName(destCity)
: destCity.name;
const cityJosa = JosaUtil.pick(cityName, '로');
const generalJosa = JosaUtil.pick(destGeneral.name, '을');
const yearMonth = resolveLastAssignment(context);
const effects: Array<GeneralActionEffect<TriggerState>> = [
@@ -117,7 +120,7 @@ export class ActionResolver<
effects.push(
createLogEffect(
`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></> 발령됐습니다.`,
`<Y>${context.general.name}</>에 의해 <G><b>${cityName}</b></>${cityJosa} 발령됐습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
@@ -128,7 +131,7 @@ export class ActionResolver<
);
effects.push(
createLogEffect(
`<Y>${destGeneral.name}</> <G><b>${cityName}</b></> 발령했습니다.`,
`<Y>${destGeneral.name}</>${generalJosa} <G><b>${cityName}</b></>${cityJosa} 발령했습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
@@ -32,6 +32,7 @@ import {
createNationPatchEffect,
} from '../../engine.js';
import { LogCategory, LogFormat, LogScope } from '../../../logging/types.js';
import { JosaUtil } from '@sammo-ts/common';
export interface AwardArgs {
isGold: boolean;
@@ -148,9 +149,10 @@ export class ActionResolver<
} as Partial<Nation>, nation.id),
];
const amountJosa = JosaUtil.pick(amountText, '을');
effects.push(
createLogEffect(
`${label} ${amountText} 포상으로 받았습니다.`,
`${label} ${amountText}${amountJosa} 포상으로 받았습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,
@@ -161,7 +163,7 @@ export class ActionResolver<
);
effects.push(
createLogEffect(
`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText} 수여했습니다.`,
`<Y>${context.destGeneral.name}</>에게 ${label} ${amountText}${amountJosa} 수여했습니다.`,
{
scope: LogScope.GENERAL,
category: LogCategory.ACTION,