feat: 레거시 재이관과 계정 복구 기반을 추가

중앙 이전 기록 스키마와 버전 정규화기를 도입하고, 재실행 시 현행 계정 상태를 보존한다. 카카오 인증 뒤 이관 비밀번호를 1회 설정하는 흐름과 비카카오 계정용 안전한 CLI 복구 경로를 추가한다.
This commit is contained in:
2026-08-17 15:55:32 +00:00
parent 50e7d894e4
commit fc7de05017
33 changed files with 1720 additions and 150 deletions
+13 -3
View File
@@ -24,6 +24,14 @@ const quoteIdentifier = (value: string): string => {
return `"${value}"`;
};
export const quoteQualifiedIdentifier = (value: string): string => {
const parts = value.split('.');
if (parts.length < 1 || parts.length > 2 || parts.some((part) => !IDENTIFIER.test(part))) {
throw new Error(`Unsafe SQL identifier: ${value}`);
}
return parts.map((part) => `"${part}"`).join('.');
};
export const createMariaPool = (uri: string): MariaPool => mariadb.createPool(uri);
export const createPostgresPool = (connectionString: string): pg.Pool => {
@@ -94,7 +102,8 @@ export const upsertRows = async (
client: PoolClient,
table: string,
rows: readonly TargetRow[],
conflictColumns: readonly string[]
conflictColumns: readonly string[],
options: { preserveOnConflict?: readonly string[] } = {}
): Promise<void> => {
if (rows.length === 0) {
return;
@@ -116,12 +125,13 @@ export const upsertRows = async (
});
return `(${placeholders.join(', ')})`;
});
const preserved = new Set(options.preserveOnConflict ?? []);
const updates = columns
.filter((column) => !conflictColumns.includes(column))
.filter((column) => !conflictColumns.includes(column) && !preserved.has(column))
.map((column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`);
const conflictAction = updates.length ? `DO UPDATE SET ${updates.join(', ')}` : 'DO NOTHING';
await client.query(
`INSERT INTO ${quoteIdentifier(table)} (${columns.map(quoteIdentifier).join(', ')})
`INSERT INTO ${quoteQualifiedIdentifier(table)} (${columns.map(quoteIdentifier).join(', ')})
VALUES ${tuples.join(', ')}
ON CONFLICT (${conflictColumns.map(quoteIdentifier).join(', ')}) ${conflictAction}`,
values