fix: stabilize backend prisma and lint

This commit is contained in:
2026-04-06 20:21:40 +09:00
parent 90ab555915
commit a8ff05c96a
35 changed files with 989 additions and 360 deletions

View File

@@ -3,7 +3,12 @@ import { PrismaService } from '../prisma/prisma.service';
import { GiteaService } from '../gitea/gitea.service';
type QaStatus = 'passed' | 'failed' | 'unknown';
type ProjectPhase = 'PLANNING' | 'IMPLEMENT' | 'QA' | 'READY FOR DEPLOY' | 'DEPLOYED';
type ProjectPhase =
| 'PLANNING'
| 'IMPLEMENT'
| 'QA'
| 'READY FOR DEPLOY'
| 'DEPLOYED';
export interface RepoDocumentMeta {
path: string;
@@ -36,7 +41,10 @@ function normalizeRepoName(repoUrl: string): string {
return repoUrl.replace(/\/+$/, '').split('/').pop() ?? '';
}
function extractDocOrder(path: string, kind: 'sprint' | 'hotfix' | 'qa'): number {
function extractDocOrder(
path: string,
kind: 'sprint' | 'hotfix' | 'qa',
): number {
const name = path.split('/').pop() ?? '';
if (kind === 'hotfix') {
const hotfix = name.match(/HOTFIX-(\d+)/i);
@@ -81,7 +89,8 @@ function parseTaskSummary(content: string): string | null {
function parseQaStatus(content: string): QaStatus {
if (/passed\s*[:=]\s*true/i.test(content)) return 'passed';
if (/passed\s*[:=]\s*false/i.test(content)) return 'failed';
if (/\bPASSED\b/i.test(content) && !/\bFAILED\b/i.test(content)) return 'passed';
if (/\bPASSED\b/i.test(content) && !/\bFAILED\b/i.test(content))
return 'passed';
if (/\bFAILED\b/i.test(content)) return 'failed';
return 'unknown';
}
@@ -89,19 +98,27 @@ function parseQaStatus(content: string): QaStatus {
function parseBlockerCount(content: string): number {
const jsonMatch = content.match(/"errors"\s*:\s*\[(.*?)\]/is);
if (jsonMatch) {
const items = jsonMatch[1].split(',').map((item) => item.trim()).filter(Boolean);
const items = jsonMatch[1]
.split(',')
.map((item) => item.trim())
.filter(Boolean);
if (items.length > 0) return items.length;
}
const errorLines = content
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => /^[-*]\s+/.test(line) && /(error|blocker|실패|문제)/i.test(line));
.filter(
(line) =>
/^[-*]\s+/.test(line) && /(error|blocker|실패|문제)/i.test(line),
);
return errorLines.length;
}
function isRepoDocumentMeta(value: RepoDocumentMeta | null): value is RepoDocumentMeta {
function isRepoDocumentMeta(
value: RepoDocumentMeta | null,
): value is RepoDocumentMeta {
return value !== null;
}
@@ -141,11 +158,12 @@ function buildProjectMeta(params: {
phase = 'IMPLEMENT';
}
const deployStatus = phase === 'DEPLOYED'
? 'DEPLOYED ON MAIN'
: phase === 'READY FOR DEPLOY'
? 'READY FOR DEPLOY'
: 'REDEPLOY REQUIRED';
const deployStatus =
phase === 'DEPLOYED'
? 'DEPLOYED ON MAIN'
: phase === 'READY FOR DEPLOY'
? 'READY FOR DEPLOY'
: 'REDEPLOY REQUIRED';
return {
phase,
@@ -177,17 +195,30 @@ export class ProjectsService {
const [repoMetaMap, repoDocsMap] = await Promise.all([
this.getRepoMetaMap(),
this.getRepoDocumentMap(dbProjects.map((project) => normalizeRepoName(project.repoUrl))),
this.getRepoDocumentMap(
dbProjects.map((project) => normalizeRepoName(project.repoUrl)),
),
]);
return dbProjects.map((project) => {
const totalSprints = project.sprints.length;
const doneSprints = project.sprints.filter((sprint) => sprint.status === 'done').length;
const inProgressSprint = project.sprints.find((sprint) => sprint.status === 'in_progress');
const ownerSister = project.sprints.flatMap((sprint) => sprint.tasks).find((task) => task.assignee)?.assignee ?? 'narang';
const progress = totalSprints > 0 ? Math.round((doneSprints / totalSprints) * 100) : 0;
const doneSprints = project.sprints.filter(
(sprint) => sprint.status === 'done',
).length;
const inProgressSprint = project.sprints.find(
(sprint) => sprint.status === 'in_progress',
);
const ownerSister =
project.sprints
.flatMap((sprint) => sprint.tasks)
.find((task) => task.assignee)?.assignee ?? 'narang';
const progress =
totalSprints > 0 ? Math.round((doneSprints / totalSprints) * 100) : 0;
const repoMeta = repoMetaMap[project.giteaId];
const docs = repoDocsMap.get(normalizeRepoName(project.repoUrl)) ?? { hotfixes: [], qas: [] };
const docs = repoDocsMap.get(normalizeRepoName(project.repoUrl)) ?? {
hotfixes: [],
qas: [],
};
const latestHotfix = docs.hotfixes.at(-1) ?? null;
const latestQa = docs.qas.at(-1) ?? null;
const meta = buildProjectMeta({
@@ -213,7 +244,11 @@ export class ProjectsService {
totalSprints,
doneSprints,
sprintCount: totalSprints,
currentSprint: inProgressSprint?.name ?? (doneSprints === totalSprints && totalSprints > 0 ? 'COMPLETED' : null),
currentSprint:
inProgressSprint?.name ??
(doneSprints === totalSprints && totalSprints > 0
? 'COMPLETED'
: null),
ownerSister,
openPRs: repoMeta?.openPRs ?? 0,
updatedAt: repoMeta?.updatedAt ?? project.updatedAt.toISOString(),
@@ -253,8 +288,12 @@ export class ProjectsService {
]);
const totalSprints = project.sprints.length;
const doneSprints = project.sprints.filter((sprint) => sprint.status === 'done').length;
const activeSprint = project.sprints.find((sprint) => sprint.status === 'in_progress');
const doneSprints = project.sprints.filter(
(sprint) => sprint.status === 'done',
).length;
const activeSprint = project.sprints.find(
(sprint) => sprint.status === 'in_progress',
);
const meta = buildProjectMeta({
totalSprints,
doneSprints,
@@ -276,7 +315,8 @@ export class ProjectsService {
label: docs.qas.at(-1)?.label ?? null,
status: docs.qas.at(-1)?.qaStatus ?? 'unknown',
blockerCount: docs.qas.at(-1)?.blockerCount ?? 0,
summary: docs.qas.at(-1)?.summary ?? docs.qas.at(-1)?.description ?? null,
summary:
docs.qas.at(-1)?.summary ?? docs.qas.at(-1)?.description ?? null,
}
: null,
deploy: {
@@ -310,17 +350,23 @@ export class ProjectsService {
const repoName = normalizeRepoName(project.repoUrl);
const docs = await this.collectRepoDocuments(repoName);
return [...docs.sprints, ...docs.hotfixes].filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order);
return [...docs.sprints, ...docs.hotfixes]
.filter(isRepoDocumentMeta)
.sort((a, b) => a.order - b.order);
}
private async getProjectMeta(id: number) {
const project = await this.prisma.project.findUnique({ where: { id }, select: { id: true, repoUrl: true } });
const project = await this.prisma.project.findUnique({
where: { id },
select: { id: true, repoUrl: true },
});
if (!project) throw new NotFoundException(`Project ${id} not found`);
return project;
}
private async getRepoMetaMap() {
const repoMetaMap: Record<number, { openPRs: number; updatedAt: string }> = {};
const repoMetaMap: Record<number, { openPRs: number; updatedAt: string }> =
{};
try {
const repos = await this.gitea.getOrgRepos();
for (const repo of repos) {
@@ -337,35 +383,80 @@ export class ProjectsService {
private async getRepoDocumentMap(repoNames: string[]) {
const uniqueRepoNames = Array.from(new Set(repoNames.filter(Boolean)));
const entries = await Promise.all(uniqueRepoNames.map(async (repoName) => [repoName, await this.collectRepoDocuments(repoName)] as const));
const entries = await Promise.all(
uniqueRepoNames.map(
async (repoName) =>
[repoName, await this.collectRepoDocuments(repoName)] as const,
),
);
return new Map(entries);
}
private async collectRepoDocuments(repoName: string) {
const [sprintPaths, hotfixPathGroups, qaPathGroups] = await Promise.all([
this.gitea.getRepoTree(repoName, SPRINT_PATH),
Promise.all(HOTFIX_PATH_CANDIDATES.map((path) => this.gitea.getRepoTree(repoName, path))),
Promise.all(QA_PATH_CANDIDATES.map((path) => this.gitea.getRepoTree(repoName, path))),
Promise.all(
HOTFIX_PATH_CANDIDATES.map((path) =>
this.gitea.getRepoTree(repoName, path),
),
),
Promise.all(
QA_PATH_CANDIDATES.map((path) =>
this.gitea.getRepoTree(repoName, path),
),
),
]);
const sprintFiles = sprintPaths.filter((path) => /SPRINT-\d+\.md$/i.test(path));
const hotfixFiles = Array.from(new Set(hotfixPathGroups.flat().filter((path) => /HOTFIX-\d+\.md$/i.test(path))));
const qaFiles = Array.from(new Set(qaPathGroups.flat().filter((path) => /(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path))));
const sprintFiles = sprintPaths.filter((path) =>
/SPRINT-\d+\.md$/i.test(path),
);
const hotfixFiles = Array.from(
new Set(
hotfixPathGroups.flat().filter((path) => /HOTFIX-\d+\.md$/i.test(path)),
),
);
const qaFiles = Array.from(
new Set(
qaPathGroups
.flat()
.filter((path) =>
/(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path),
),
),
);
const [sprints, hotfixes, qas] = await Promise.all([
Promise.all(sprintFiles.map((path) => this.readRepoDocument(repoName, path, 'sprint'))),
Promise.all(hotfixFiles.map((path) => this.readRepoDocument(repoName, path, 'hotfix'))),
Promise.all(qaFiles.map((path) => this.readRepoDocument(repoName, path, 'qa'))),
Promise.all(
sprintFiles.map((path) =>
this.readRepoDocument(repoName, path, 'sprint'),
),
),
Promise.all(
hotfixFiles.map((path) =>
this.readRepoDocument(repoName, path, 'hotfix'),
),
),
Promise.all(
qaFiles.map((path) => this.readRepoDocument(repoName, path, 'qa')),
),
]);
return {
sprints: sprints.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
hotfixes: hotfixes.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
sprints: sprints
.filter(isRepoDocumentMeta)
.sort((a, b) => a.order - b.order),
hotfixes: hotfixes
.filter(isRepoDocumentMeta)
.sort((a, b) => a.order - b.order),
qas: qas.filter(isRepoDocumentMeta).sort((a, b) => a.order - b.order),
};
}
private async readRepoDocument(repoName: string, path: string, kind: 'sprint' | 'hotfix' | 'qa'): Promise<RepoDocumentMeta | null> {
private async readRepoDocument(
repoName: string,
path: string,
kind: 'sprint' | 'hotfix' | 'qa',
): Promise<RepoDocumentMeta | null> {
const content = await this.gitea.getRawFile(repoName, path);
if (!content) return null;