feat: 전사 공유 일정(주간 캘린더) 기능 추가

상단 nav 독립 메뉴 '일정' — 주간 타임라인(간트형) 캘린더.
카테고리=레인, 일정=막대(다중일 지원), 우측 상세 패널, 일정/카테고리 CRUD.

백엔드(Schedule 모듈):
- ScheduleCategory/ScheduleEvent 엔티티 + 참석자(다대다) + 작성자
- 카테고리/일정 CRUD, 기간 겹침 조회, 참석자 풀(전사 사용자)
- 기본 카테고리 12종 onModuleInit 멱등 시드
- 인증 사용자 누구나 관리(전역 일정, 글로벌 관리자 개념 없음)
- 운영 마이그레이션(AddSchedule) 추가, @nestjs/schedule 임포트는 CronModule 로 alias

프론트:
- /schedule 라우트 + 사이드바 nav 항목
- 주간 네비(이전/다음/오늘), 유형 필터·카테고리 관리 드롭다운
- 타임라인/상세 패널/일정 모달/카테고리 모달(hue 슬라이더)/참석자 멀티셀렉트
- schedule.store + useSchedule + scheduleDate 유틸

런타임 검증: dev 부팅·시드(12건)·라우트 매핑 정상, 인증 게이트(401),
일정 생성/조회/수정/삭제 + 참석자/전직원/다중일 스모크 전부 정상.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:23:10 +09:00
parent 62df92e6eb
commit 346ec75553
22 changed files with 3889 additions and 5 deletions
+94
View File
@@ -0,0 +1,94 @@
// 일정(주간 캘린더) 날짜 유틸 — 모두 'YYYY-MM-DD' 로컬 날짜 문자열 기준(시간대 이동 방지).
// 월요일 시작 요일 라벨(월~일)
export const DOW_KO = ['월', '화', '수', '목', '금', '토', '일'] as const
// Date → 로컬 'YYYY-MM-DD'
export function isoOf(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
return `${y}-${m}-${d}`
}
// 'YYYY-MM-DD' → 로컬 Date(자정)
export function dateOf(iso: string): Date {
const [y, m, d] = iso.split('-').map(Number)
return new Date(y ?? 1970, (m ?? 1) - 1, d ?? 1)
}
// 오늘(로컬) ISO
export function todayIso(): string {
return isoOf(new Date())
}
// iso 가 속한 주의 월요일 ISO
export function mondayOf(iso: string): string {
const dt = dateOf(iso)
// getDay(): 0=일 … 6=토 → 월요일까지 거슬러 올라갈 일수
const back = (dt.getDay() + 6) % 7
dt.setDate(dt.getDate() - back)
return isoOf(dt)
}
// iso + n일
export function addDays(iso: string, n: number): string {
const dt = dateOf(iso)
dt.setDate(dt.getDate() + n)
return isoOf(dt)
}
// 요일 인덱스(0=월 … 6=일)
function dowIndex(iso: string): number {
return (dateOf(iso).getDay() + 6) % 7
}
export interface WeekDay {
iso: string
dow: string
d: number
weekend: boolean
isToday: boolean
}
// 주 시작(월요일) ISO → 7일 배열
export function weekDays(weekStartIso: string): WeekDay[] {
const today = todayIso()
return Array.from({ length: 7 }, (_, i) => {
const iso = addDays(weekStartIso, i)
return {
iso,
dow: DOW_KO[i] ?? '',
d: dateOf(iso).getDate(),
weekend: i >= 5,
isToday: iso === today,
}
})
}
// 주 범위 라벨 — 예: '2026년 6월 22 28일' / 월이 다르면 '6월 29 7월 5일'
export function weekRangeLabel(weekStartIso: string): string {
const s = dateOf(weekStartIso)
const e = dateOf(addDays(weekStartIso, 6))
const sm = s.getMonth() + 1
const em = e.getMonth() + 1
if (sm === em) {
return `${s.getFullYear()}${sm}${s.getDate()} ${e.getDate()}`
}
return `${s.getFullYear()}${sm}${s.getDate()} ${em}${e.getDate()}`
}
// 단일/다중일 표시용 — '6월 22일 (월)' 또는 '6월 22일(월) 24일(수)'
export function eventDateText(start: string, end: string): string {
const s = dateOf(start)
if (start === end) {
return `${s.getMonth() + 1}${s.getDate()}일 (${DOW_KO[dowIndex(start)]})`
}
const e = dateOf(end)
return `${s.getMonth() + 1}${s.getDate()}일(${DOW_KO[dowIndex(start)]}) ${e.getMonth() + 1}${e.getDate()}일(${DOW_KO[dowIndex(end)]})`
}
// 다중일 일수(포함 양끝)
export function spanDays(start: string, end: string): number {
return Math.round((dateOf(end).getTime() - dateOf(start).getTime()) / 86400000) + 1
}