EENE Dashboard upload to Gitea
This commit is contained in:
@@ -261,10 +261,9 @@ function buildMilestones(p: HrProject): MappedTask['milestones'] {
|
||||
return milestones;
|
||||
}
|
||||
|
||||
function buildDetailContent(p: HrProject): string | null {
|
||||
const content = p.progressStatus?.trim() || p.progressLog?.trim();
|
||||
if (!content || content === '이슈사항' || content === '12') return null;
|
||||
return content;
|
||||
/** @deprecated progressStatus는 milestone·periodEntries로 이관 — TaskDetail(피드백)에 넣지 않음 */
|
||||
function buildDetailContent(_p: HrProject): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mapHrProjectToTask(p: HrProject, quarter = '2026-Q2'): MappedTask {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "tasks" ADD COLUMN "detailDescription" TEXT;
|
||||
|
||||
-- 기존 description(첫 줄=개요, 이후=상세) → 분리 (실행과제·프로젝트만)
|
||||
UPDATE "tasks"
|
||||
SET
|
||||
"detailDescription" = CASE
|
||||
WHEN position(E'\n' in "description") > 0
|
||||
THEN NULLIF(trim(substring("description" from position(E'\n' in "description") + 1)), '')
|
||||
ELSE NULL
|
||||
END,
|
||||
"description" = NULLIF(trim(split_part("description", E'\n', 1)), '')
|
||||
WHERE "description" IS NOT NULL
|
||||
AND trim("description") != ''
|
||||
AND ("taskType" = '실행과제' OR "taskType" = '프로젝트');
|
||||
@@ -66,6 +66,7 @@ model Task {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
description String?
|
||||
detailDescription String?
|
||||
status TaskStatus @default(TODO)
|
||||
priority Priority @default(MEDIUM)
|
||||
quarter String // 예: "2026-Q2"
|
||||
|
||||
54
backend/scripts/cleanup-legacy-task-details.ts
Normal file
54
backend/scripts/cleanup-legacy-task-details.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* HR import 시 TaskDetail(피드백)에 잘못 들어간 progressStatus 레거시 삭제
|
||||
* — milestoneId 없고 authorName 없는 행 (seed 자동 생성분)
|
||||
*
|
||||
* npx tsx scripts/cleanup-legacy-task-details.ts
|
||||
* npx tsx scripts/cleanup-legacy-task-details.ts --dry-run
|
||||
*/
|
||||
import 'dotenv/config';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
|
||||
async function main() {
|
||||
const legacy = await prisma.taskDetail.findMany({
|
||||
where: {
|
||||
milestoneId: null,
|
||||
OR: [{ authorName: null }, { authorName: '' }],
|
||||
},
|
||||
include: { task: { select: { title: true } } },
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
|
||||
if (legacy.length === 0) {
|
||||
console.log('✅ 삭제할 레거시 피드백 없음');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`레거시 TaskDetail ${legacy.length}건 (일정 미연결 · 작성자 없음):`);
|
||||
for (const row of legacy) {
|
||||
const preview = row.content.replace(/\s+/g, ' ').slice(0, 72);
|
||||
console.log(` · [${row.task.title}] ${preview}${row.content.length > 72 ? '…' : ''}`);
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log('\n(dry-run — 삭제하지 않음. 적용: npx tsx scripts/cleanup-legacy-task-details.ts)');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await prisma.taskDetail.deleteMany({
|
||||
where: {
|
||||
id: { in: legacy.map((r) => r.id) },
|
||||
},
|
||||
});
|
||||
|
||||
console.log(`\n✅ ${result.count}건 삭제 완료`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
77
backend/scripts/normalize-task-sections.ts
Normal file
77
backend/scripts/normalize-task-sections.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* task.section 레거시 값 정규화 + 조직문화(EX) 재배치
|
||||
* npx tsx scripts/normalize-task-sections.ts
|
||||
*/
|
||||
import 'dotenv/config';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const EX_TITLE = /회사생활|C\.E\.L|조직문화|복리후생|문화\s*진단|직원\s*소통/i;
|
||||
|
||||
const SECTION_MAP: Record<string, string> = {
|
||||
성장지원: '학습성장',
|
||||
HR: '인사관리',
|
||||
운영지원: '운영관리',
|
||||
전산관리: '운영관리',
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const tasks = await prisma.task.findMany({
|
||||
select: { id: true, title: true, section: true, category: true, taskType: true },
|
||||
});
|
||||
|
||||
let renamed = 0;
|
||||
let exMoved = 0;
|
||||
|
||||
for (const task of tasks) {
|
||||
const section = task.section?.trim() ?? '';
|
||||
let nextSection = SECTION_MAP[section] ?? section;
|
||||
|
||||
if (nextSection !== '조직문화' && EX_TITLE.test(task.title.trim())) {
|
||||
nextSection = '조직문화';
|
||||
exMoved += 1;
|
||||
}
|
||||
|
||||
const patch: { section?: string; category?: string | null } = {};
|
||||
if (nextSection && nextSection !== section) {
|
||||
patch.section = nextSection;
|
||||
renamed += 1;
|
||||
}
|
||||
if (nextSection === '조직문화' && task.category !== '조직문화') {
|
||||
patch.category = '조직문화';
|
||||
}
|
||||
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await prisma.task.update({ where: { id: task.id }, data: patch });
|
||||
console.log(` section: ${section || '(empty)'} → ${nextSection} | ${task.title}`);
|
||||
}
|
||||
}
|
||||
|
||||
const col = await prisma.columnConfig.findUnique({ where: { key: '운영관리' } });
|
||||
if (col && (col.title === '운영관리' || col.title === '운영관리 부문' || col.titleEn === 'Operations')) {
|
||||
await prisma.columnConfig.update({
|
||||
where: { key: '운영관리' },
|
||||
data: { title: '총무관리', titleEn: 'GA' },
|
||||
});
|
||||
console.log(' columnConfig 운영관리 → 총무관리');
|
||||
}
|
||||
|
||||
const hrd = await prisma.columnConfig.findUnique({ where: { key: '학습성장' } });
|
||||
if (hrd && (hrd.title === '학습성장' || hrd.title === '성장지원' || hrd.titleEn === 'Learning & Growth')) {
|
||||
await prisma.columnConfig.update({
|
||||
where: { key: '학습성장' },
|
||||
data: { title: '인재육성', titleEn: 'HRD' },
|
||||
});
|
||||
console.log(' columnConfig 학습성장 → 인재육성');
|
||||
}
|
||||
|
||||
console.log(`\n✅ normalize-task-sections complete (${renamed} renamed, ${exMoved} → 조직문화)`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
@@ -2,12 +2,20 @@ export interface TaskIssueEntry {
|
||||
id: string;
|
||||
text: string;
|
||||
showOnCard: boolean;
|
||||
occurredOn?: string | null;
|
||||
}
|
||||
|
||||
function newIssueId() {
|
||||
return `issue-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
function normalizeOccurredOn(raw: unknown): string | null {
|
||||
if (typeof raw !== 'string' || !raw.trim()) return null;
|
||||
const iso = raw.trim();
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return null;
|
||||
return iso;
|
||||
}
|
||||
|
||||
export function normalizeIssueEntries(raw: unknown): TaskIssueEntry[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const entries: TaskIssueEntry[] = [];
|
||||
@@ -20,6 +28,7 @@ export function normalizeIssueEntries(raw: unknown): TaskIssueEntry[] {
|
||||
id: typeof row.id === 'string' && row.id ? row.id : newIssueId(),
|
||||
text,
|
||||
showOnCard: row.showOnCard !== false,
|
||||
occurredOn: normalizeOccurredOn(row.occurredOn),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
@@ -34,7 +43,7 @@ export function parseIssueEntriesFromTask(task: {
|
||||
if (fromJson.length > 0) return fromJson;
|
||||
const legacy = task.issueNote?.trim();
|
||||
if (!legacy) return [];
|
||||
return [{ id: 'legacy', text: legacy, showOnCard: task.showIssue !== false }];
|
||||
return [{ id: 'legacy', text: legacy, showOnCard: task.showIssue !== false, occurredOn: null }];
|
||||
}
|
||||
|
||||
export function deriveIssueFields(entries: TaskIssueEntry[]) {
|
||||
|
||||
@@ -25,6 +25,18 @@ export const taskInclude = {
|
||||
taskAssignees: {
|
||||
include: { member: { select: teamMemberSelect } },
|
||||
},
|
||||
milestones: {
|
||||
orderBy: { order: 'asc' as const },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
progress: true,
|
||||
startDate: true,
|
||||
dueDate: true,
|
||||
periodEntries: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
_count: { select: { files: true, details: true } },
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ router.get('/:id', async (req, res, next) => {
|
||||
router.post('/', async (req, res, next) => {
|
||||
try {
|
||||
const body = req.body as Record<string, any>;
|
||||
const { title, description, status, priority, quarter, category,
|
||||
const { title, description, detailDescription, status, priority, quarter, category,
|
||||
section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate,
|
||||
showDescription, showStatus, showIssue, showProgress, pmMemberId } = body;
|
||||
|
||||
@@ -88,6 +88,7 @@ router.post('/', async (req, res, next) => {
|
||||
data: {
|
||||
title,
|
||||
description,
|
||||
detailDescription: detailDescription ?? null,
|
||||
status: (status as any) || 'TODO',
|
||||
priority: (priority as any) || 'MEDIUM',
|
||||
quarter,
|
||||
@@ -135,7 +136,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
if (!existing) throw new AppError(404, '업무를 찾을 수 없습니다.');
|
||||
|
||||
const body = req.body as Record<string, any>;
|
||||
const { title, description, status, priority, quarter, category,
|
||||
const { title, description, detailDescription, status, priority, quarter, category,
|
||||
section, tag, taskType, progress, issueNote, startDate, dueDate, assigneeId, showDate,
|
||||
showDescription, showStatus, showIssue, showProgress, pmMemberId } = body;
|
||||
|
||||
@@ -147,6 +148,7 @@ router.patch('/:id', async (req, res, next) => {
|
||||
data: {
|
||||
...(title && { title }),
|
||||
...(description !== undefined && { description }),
|
||||
...(detailDescription !== undefined && { detailDescription: detailDescription || null }),
|
||||
...(status && { status: status as any }),
|
||||
...(priority && { priority: priority as any }),
|
||||
...(quarter && { quarter }),
|
||||
|
||||
Reference in New Issue
Block a user