Compare commits

...

4 Commits

Author SHA1 Message Date
EENE Dashboard
c31eca4b58 EENE Dashboard upload to Gitea 2026-06-18 15:06:37 +09:00
EENE Dashboard
d3548cf7ff EENE Dashboard upload to Gitea 2026-06-18 12:05:08 +09:00
EENE Dashboard
29ba4867bf Track local data: uploads and data/postgres for Gitea clone
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 17:07:19 +09:00
EENE Dashboard
a1f70a5b46 Fix Gitea upload script to use SSH port 222
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 17:01:20 +09:00
1449 changed files with 11141 additions and 1256 deletions

9
.gitignore vendored
View File

@@ -3,13 +3,10 @@ dist/
build/
.env
backend/.env
data/postgres/
!data/.gitkeep
!data/seed/
!data/seed/**
uploads/*
!uploads/.gitkeep
*.log
.DS_Store
Thumbs.db
frontend/.lan-ip
# DB lock files (recreated on server start)
data/postgres/postmaster.pid
data/postgres/postmaster.opts

View File

@@ -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 {

View File

@@ -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" = '프로젝트');

View File

@@ -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"

View File

@@ -42,7 +42,7 @@ const HUB_CONFIG = {
],
routineLabels: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
routineLabels: ['채용 운영', '학습 지원', '운영 지원', '자산·시설', '문서·행정'],
};

View 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());

View 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());

View File

@@ -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[]) {

View File

@@ -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;

View File

@@ -13,11 +13,22 @@ export const DEFAULT_HUB_CONFIG = {
{ id: '2', date: '2026-05-15', text: '조직문화 진단·리더십 교육' },
{ id: '3', date: '2026-06-20', text: '분기 성과 점검·평가' },
],
routineLabels: ['채용 운영', '학습 지원', '직원 소통', '자산·시설', '문서·행정'],
routineLabels: ['채용 운영', '학습 지원', '운영 지원', '자산·시설', '문서·행정'],
};
function migrateRoutineLabels(labels: string[]): string[] {
return labels.map((label) => {
if (label === '교육 운영') return '학습 지원';
if (label === '직원 소통') return '운영 지원';
return label;
});
}
function normalizeConfig(raw: Record<string, unknown>) {
const sloganTitle = (raw.sloganTitle as string) ?? DEFAULT_HUB_CONFIG.sloganTitle;
const routineLabels = Array.isArray(raw.routineLabels)
? migrateRoutineLabels(raw.routineLabels as string[])
: DEFAULT_HUB_CONFIG.routineLabels;
return {
sloganTitle: sloganTitle === '분기 슬로건' ? '분기 중점 과제' : sloganTitle,
sloganLines: Array.isArray(raw.sloganLines)
@@ -27,9 +38,7 @@ function normalizeConfig(raw: Record<string, unknown>) {
scheduleItems: Array.isArray(raw.scheduleItems)
? (raw.scheduleItems as typeof DEFAULT_HUB_CONFIG.scheduleItems)
: DEFAULT_HUB_CONFIG.scheduleItems,
routineLabels: Array.isArray(raw.routineLabels)
? (raw.routineLabels as string[])
: DEFAULT_HUB_CONFIG.routineLabels,
routineLabels,
};
}

View File

@@ -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 }),

1
data/postgres/PG_VERSION Normal file
View File

@@ -0,0 +1 @@
16

BIN
data/postgres/base/1/112 Normal file

Binary file not shown.

BIN
data/postgres/base/1/113 Normal file

Binary file not shown.

BIN
data/postgres/base/1/1247 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/1249 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/1255 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/1259 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

View File

BIN
data/postgres/base/1/14928 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/14932 Normal file

Binary file not shown.

BIN
data/postgres/base/1/14933 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/14937 Normal file

Binary file not shown.

BIN
data/postgres/base/1/14938 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/14942 Normal file

Binary file not shown.

BIN
data/postgres/base/1/14943 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/14947 Normal file

Binary file not shown.

BIN
data/postgres/base/1/174 Normal file

Binary file not shown.

BIN
data/postgres/base/1/175 Normal file

Binary file not shown.

BIN
data/postgres/base/1/2187 Normal file

Binary file not shown.

View File

BIN
data/postgres/base/1/2228 Normal file

Binary file not shown.

View File

View File

BIN
data/postgres/base/1/2337 Normal file

Binary file not shown.

BIN
data/postgres/base/1/2579 Normal file

Binary file not shown.

BIN
data/postgres/base/1/2600 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2601 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2602 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2603 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/2605 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2606 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2607 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2608 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2609 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2610 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/2612 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

BIN
data/postgres/base/1/2615 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2616 Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/postgres/base/1/2617 Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More