diff --git a/frontend/src/pages/DetailMockPage.tsx b/frontend/src/pages/DetailMockPage.tsx new file mode 100644 index 0000000..fd3648b --- /dev/null +++ b/frontend/src/pages/DetailMockPage.tsx @@ -0,0 +1,428 @@ +/** + * 레이아웃 목업 — API/DB 연동 없음 + * 확인: /detail-mock + */ + +import { useMemo, useState } from 'react'; + +interface Feedback { + author: string; + text: string; + date: string; +} + +interface ContentItem { + text: string; + date: string; +} + +interface Stage { + id: string; + title: string; + range: string; + progress: number; + note: string; + updatedAt: string; + contents: ContentItem[]; + feedbacks: Feedback[]; + previewHint: string; + timeline: { left: number; width: number; label: string }; +} + +const MOCK = { + title: '가족사 인원현황 대시보드 기획', + assignee: '정성호', + period: '2026-06-01 ~ 2026-06-30', + status: '진행중', + section: '인사관리', + overview: + '가족사 전체 인원 현황을 한눈에 파악할 수 있는 대시보드를 기획·구축합니다. 부서·직급·연령·근속 등 핵심 지표를 시각화합니다.', + timeline: { start: '6.01', end: '6.30', today: '6.05', todayLeft: 13 }, + stages: [ + { + id: '1', + title: '대시보드 구성 기획', + range: '06/01 ~ 06/16', + progress: 80, + note: '와이어프레임 1차 확정', + updatedAt: '06/05', + contents: [ + { text: '화면 흐름도·메뉴 구조 확정', date: '06/05' }, + { text: '와이어프레임 1차 작성 및 팀 리뷰', date: '06/04' }, + { text: '대시보드 KPI 영역·차트 배치 설계', date: '06/01' }, + ], + feedbacks: [ + { author: '정성호', text: '와이어프레임 v2 공유 완료', date: '06/05' }, + { author: '김기획', text: 'KPI 카드 4개로 축소 제안 — 승인', date: '06/03' }, + ], + previewHint: '와이어프레임 / 기획서 PDF', + timeline: { left: 0, width: 53, label: '① 기획' }, + }, + { + id: '2', + title: '인사 데이터 수집·연동', + range: '06/08 ~ 06/12', + progress: 45, + note: 'API 연동 일정 협의 중', + updatedAt: '06/08', + contents: [ + { text: '매핑표 1차본 HR팀 송부', date: '06/08' }, + { text: '개인정보 마스킹 정책 검토', date: '06/07' }, + { text: 'HR 원본 데이터 컬럼 매핑표 작성', date: '06/06' }, + ], + feedbacks: [ + { author: '정성호', text: '매핑표 1차본 HR팀 송부', date: '06/08' }, + { author: '이개발', text: '업로드 API 스펙 초안 전달 예정', date: '06/07' }, + ], + previewHint: '데이터 매핑표 · 샘플 CSV', + timeline: { left: 23, width: 17, label: '② 데이터' }, + }, + { + id: '3', + title: 'UI·시각화 데이터 구성', + range: '06/22 ~ 06/26', + progress: 0, + note: '', + updatedAt: '06/22', + contents: [ + { text: 'Wide 사이니지용 레이아웃 분기', date: '06/22' }, + { text: '부서·직급·연령대 시각화 구현', date: '06/21' }, + { text: '차트 컴포넌트 라이브러리 선정', date: '06/20' }, + ], + feedbacks: [], + previewHint: 'UI 목업 · 차트 스크린샷', + timeline: { left: 70, width: 13, label: '③ UI' }, + }, + { + id: '4', + title: '데이터 검증·오류 수정', + range: '06/26 ~ 06/30', + progress: 0, + note: '', + updatedAt: '06/26', + contents: [ + { text: '최종 QA 및 인수', date: '06/26' }, + { text: '오류 케이스 목록화 및 수정', date: '06/25' }, + { text: '실데이터 vs 대시보드 수치 대조', date: '06/24' }, + ], + feedbacks: [ + { author: '김기획', text: '검증 체크리스트 템플릿 공유 예정', date: '06/20' }, + ], + previewHint: 'QA 체크리스트 · 검증 리포트', + timeline: { left: 83, width: 17, label: '④ 검증' }, + }, + ] satisfies Stage[], +}; + +function dateKey(md: string): number { + const [m, d] = md.split('/').map(Number); + return m * 100 + d; +} + +function sortByDateDesc(items: T[], pick: (item: T) => string): T[] { + return [...items].sort((a, b) => dateKey(pick(b)) - dateKey(pick(a))); +} + +function Badge({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( + + {children} + + ); +} + +function PanelLabel({ children, sub }: { children: React.ReactNode; sub?: string }) { + return ( +
+

{children}

+ {sub && {sub}} +
+ ); +} + +const PREVIEW_GRADIENTS = [ + 'from-slate-800 via-slate-700 to-emerald-900/50', + 'from-slate-800 via-blue-900/40 to-slate-700', + 'from-slate-800 via-violet-900/30 to-slate-700', + 'from-slate-800 via-amber-900/25 to-slate-700', +]; + +/** 좌측 4구역 — 스크롤 없이 고정 비율 */ +function LeftSection({ + ratio, + children, + className = '', +}: { + ratio: '1' | '2'; + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export default function DetailMockPage() { + const sortedStages = useMemo( + () => sortByDateDesc(MOCK.stages, (s) => s.updatedAt), + [], + ); + + const [selectedId, setSelectedId] = useState(sortedStages[0].id); + const selected = sortedStages.find((s) => s.id === selectedId) ?? sortedStages[0]; + const stageIndex = sortedStages.findIndex((s) => s.id === selectedId); + + const sortedContents = useMemo( + () => sortByDateDesc(selected.contents, (c) => c.date), + [selected], + ); + const sortedFeedbacks = useMemo( + () => sortByDateDesc(selected.feedbacks, (f) => f.date), + [selected], + ); + + return ( +
+ {/* 상단 바 */} +
+
+
+ +

+ {MOCK.title} +

+ +
+ + 담당{' '} + {MOCK.assignee} + + + + 수행기간{' '} + {MOCK.period} + + + + 부서{' '} + {MOCK.section} + + {MOCK.status} +
+
+ + {/* 본문 — 좌 1/4 · 우 3/4 풀사이즈 */} +
+ {/* 좌: 1/5 · 2/5 · 2/5 · 1/5 (스크롤 없음) */} +