diff --git a/src/frontend/public/mock/booth_design_studio/img1.jpg b/src/frontend/public/mock/booth_design_studio/img1.jpg new file mode 100644 index 0000000..7a387c8 Binary files /dev/null and b/src/frontend/public/mock/booth_design_studio/img1.jpg differ diff --git a/src/frontend/public/mock/booth_design_studio/img2.jpg b/src/frontend/public/mock/booth_design_studio/img2.jpg new file mode 100644 index 0000000..8513724 Binary files /dev/null and b/src/frontend/public/mock/booth_design_studio/img2.jpg differ diff --git a/src/frontend/public/mock/booth_design_studio/img3.jpg b/src/frontend/public/mock/booth_design_studio/img3.jpg new file mode 100644 index 0000000..e354aea Binary files /dev/null and b/src/frontend/public/mock/booth_design_studio/img3.jpg differ diff --git a/src/frontend/public/mock/booth_design_studio/img4.jpg b/src/frontend/public/mock/booth_design_studio/img4.jpg new file mode 100644 index 0000000..10551f2 Binary files /dev/null and b/src/frontend/public/mock/booth_design_studio/img4.jpg differ diff --git a/src/frontend/public/mock/compliance_report/img1.jpg b/src/frontend/public/mock/compliance_report/img1.jpg new file mode 100644 index 0000000..a82bd53 Binary files /dev/null and b/src/frontend/public/mock/compliance_report/img1.jpg differ diff --git a/src/frontend/public/mock/compliance_report/img2.jpg b/src/frontend/public/mock/compliance_report/img2.jpg new file mode 100644 index 0000000..f4cec01 Binary files /dev/null and b/src/frontend/public/mock/compliance_report/img2.jpg differ diff --git a/src/frontend/public/mock/exhibitor_home/img1.jpg b/src/frontend/public/mock/exhibitor_home/img1.jpg new file mode 100644 index 0000000..1a6e940 Binary files /dev/null and b/src/frontend/public/mock/exhibitor_home/img1.jpg differ diff --git a/src/frontend/public/mock/exhibitor_home/img2.jpg b/src/frontend/public/mock/exhibitor_home/img2.jpg new file mode 100644 index 0000000..bc721c8 Binary files /dev/null and b/src/frontend/public/mock/exhibitor_home/img2.jpg differ diff --git a/src/frontend/public/mock/exhibitor_home/img3.jpg b/src/frontend/public/mock/exhibitor_home/img3.jpg new file mode 100644 index 0000000..7a387c8 Binary files /dev/null and b/src/frontend/public/mock/exhibitor_home/img3.jpg differ diff --git a/src/frontend/public/mock/layout_comparison/img1.jpg b/src/frontend/public/mock/layout_comparison/img1.jpg new file mode 100644 index 0000000..10551f2 Binary files /dev/null and b/src/frontend/public/mock/layout_comparison/img1.jpg differ diff --git a/src/frontend/public/mock/manager_approval_queue/img1.jpg b/src/frontend/public/mock/manager_approval_queue/img1.jpg new file mode 100644 index 0000000..3198481 Binary files /dev/null and b/src/frontend/public/mock/manager_approval_queue/img1.jpg differ diff --git a/src/frontend/public/mock/manager_field_inspection/img1.jpg b/src/frontend/public/mock/manager_field_inspection/img1.jpg new file mode 100644 index 0000000..fb76e1c Binary files /dev/null and b/src/frontend/public/mock/manager_field_inspection/img1.jpg differ diff --git a/src/frontend/public/mock/manager_field_inspection/img2.jpg b/src/frontend/public/mock/manager_field_inspection/img2.jpg new file mode 100644 index 0000000..0b60301 Binary files /dev/null and b/src/frontend/public/mock/manager_field_inspection/img2.jpg differ diff --git a/src/frontend/public/mock/manager_field_inspection/img3.jpg b/src/frontend/public/mock/manager_field_inspection/img3.jpg new file mode 100644 index 0000000..4a33653 Binary files /dev/null and b/src/frontend/public/mock/manager_field_inspection/img3.jpg differ diff --git a/src/frontend/public/mock/manager_field_inspection/img4.jpg b/src/frontend/public/mock/manager_field_inspection/img4.jpg new file mode 100644 index 0000000..cc42548 Binary files /dev/null and b/src/frontend/public/mock/manager_field_inspection/img4.jpg differ diff --git a/src/frontend/public/mock/organizer_dashboard/img1.jpg b/src/frontend/public/mock/organizer_dashboard/img1.jpg new file mode 100644 index 0000000..e354aea Binary files /dev/null and b/src/frontend/public/mock/organizer_dashboard/img1.jpg differ diff --git a/src/frontend/public/mock/organizer_dashboard/img2.jpg b/src/frontend/public/mock/organizer_dashboard/img2.jpg new file mode 100644 index 0000000..2cdbb75 Binary files /dev/null and b/src/frontend/public/mock/organizer_dashboard/img2.jpg differ diff --git a/src/frontend/public/mock/organizer_dashboard/img3.jpg b/src/frontend/public/mock/organizer_dashboard/img3.jpg new file mode 100644 index 0000000..10551f2 Binary files /dev/null and b/src/frontend/public/mock/organizer_dashboard/img3.jpg differ diff --git a/src/frontend/public/mock/review_details/img1.jpg b/src/frontend/public/mock/review_details/img1.jpg new file mode 100644 index 0000000..2446043 Binary files /dev/null and b/src/frontend/public/mock/review_details/img1.jpg differ diff --git a/src/frontend/public/mock/utility_order_summary/img1.jpg b/src/frontend/public/mock/utility_order_summary/img1.jpg new file mode 100644 index 0000000..b407cfd Binary files /dev/null and b/src/frontend/public/mock/utility_order_summary/img1.jpg differ diff --git a/src/frontend/public/mock/visualization_gallery/img1.jpg b/src/frontend/public/mock/visualization_gallery/img1.jpg new file mode 100644 index 0000000..e4e96b6 Binary files /dev/null and b/src/frontend/public/mock/visualization_gallery/img1.jpg differ diff --git a/src/frontend/public/mock/visualization_gallery/img2.jpg b/src/frontend/public/mock/visualization_gallery/img2.jpg new file mode 100644 index 0000000..e455cae Binary files /dev/null and b/src/frontend/public/mock/visualization_gallery/img2.jpg differ diff --git a/src/frontend/public/mock/visualization_gallery/img3.jpg b/src/frontend/public/mock/visualization_gallery/img3.jpg new file mode 100644 index 0000000..238217b Binary files /dev/null and b/src/frontend/public/mock/visualization_gallery/img3.jpg differ diff --git a/src/frontend/public/mock/visualization_gallery/img4.jpg b/src/frontend/public/mock/visualization_gallery/img4.jpg new file mode 100644 index 0000000..bbac4a2 Binary files /dev/null and b/src/frontend/public/mock/visualization_gallery/img4.jpg differ diff --git a/src/frontend/public/mock/visualization_gallery/img5.jpg b/src/frontend/public/mock/visualization_gallery/img5.jpg new file mode 100644 index 0000000..021cd50 Binary files /dev/null and b/src/frontend/public/mock/visualization_gallery/img5.jpg differ diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 88cc687..d21b84f 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -1,11 +1,19 @@ -import { Navigate, Route, Routes } from 'react-router-dom'; +import { Navigate, Outlet, Route, Routes } from 'react-router-dom'; import { useAuthStore } from './store/authStore'; import { LoginPage } from './screens/login/LoginPage'; import { BoothLayoutEditorPage } from './screens/floorplan/BoothLayoutEditorPage'; +import { LayoutComparisonPage } from './screens/floorplan/LayoutComparisonPage'; import { OrganizerDashboardPage } from './screens/dashboard/OrganizerDashboardPage'; +import { ExhibitorHomePage } from './screens/exhibitor/ExhibitorHomePage'; import { BoothDesignStudioPage } from './screens/design/BoothDesignStudioPage'; import { UtilityWiringPage } from './screens/utility/UtilityWiringPage'; +import { UtilityOrderSummaryPage } from './screens/utility/UtilityOrderSummaryPage'; +import { ComplianceReportPage } from './screens/compliance/ComplianceReportPage'; +import { ManagerApprovalQueuePage } from './screens/approval/ManagerApprovalQueuePage'; +import { ReviewDetailsPage } from './screens/approval/ReviewDetailsPage'; import { VisualizationGalleryPage } from './screens/gallery/VisualizationGalleryPage'; +import { ContractorOnsiteChecklistPage } from './screens/mobile/ContractorOnsiteChecklistPage'; +import { ManagerFieldInspectionPage } from './screens/mobile/ManagerFieldInspectionPage'; import { AppShell } from './components/layout/AppShell'; /** 인증 가드 — 미인증 시 로그인으로. (역할별 라우팅은 화면 추가 시 확장) */ @@ -20,7 +28,7 @@ export function App() { } /> - {/* 인증 필요 — 사이드바 셸 안 */} + {/* 인증 필요 — 사이드바 셸 안 (데스크톱 1440) */} @@ -30,21 +38,61 @@ export function App() { > {/* SCR-02 주최자 대시보드 */} } /> + {/* SCR-05 참가업체 홈 */} + } /> {/* SCR-03 부스 배치 에디터 (M2) */} } /> + {/* SCR-04 AI 배치안 비교 */} + } + /> {/* SCR-06 부스 설계 스튜디오 (M3+M5) */} } /> {/* SCR-07 유틸리티 배선 뷰 (M4a+M4b) */} } /> + {/* SCR-08 유틸리티 신청 요약 */} + } + /> + {/* SCR-09 규정 검증 결과 */} + } + /> + {/* SCR-10 검수 대기열 (홀매니저) */} + } + /> + {/* SCR-11 검수 상세 */} + } + /> {/* SCR-12 시각화 갤러리 (M5) */} } /> - {/* 향후: SCR-05 부스홈, SCR-10 승인큐 등 */} + + + {/* 모바일 전용 (390) — 셸 없이 전폭, 조회·현장 */} + + + + } + > + {/* SCR-M1 장치업체 현장 체크리스트 */} + } /> + {/* SCR-M2 홀매니저 현장 검수 */} + } /> } /> diff --git a/src/frontend/src/api/endpoints.ts b/src/frontend/src/api/endpoints.ts index 9286ef4..c78c435 100644 --- a/src/frontend/src/api/endpoints.ts +++ b/src/frontend/src/api/endpoints.ts @@ -11,13 +11,17 @@ import type { DesignPlanDto, DesignSaveRequest, DesignSpec, + ForgotPasswordRequest, KintexPrincipal, LayoutDto, LayoutSaveRequest, LoginRequest, LoginResponse, + RegisterRequest, + RegisterResponse, RenderJobDto, RenderJobRequest, + ResetPasswordRequest, UtilityOrderDto, UtilityQuote, UtilityQuoteRequest, @@ -34,6 +38,15 @@ export const authApi = { me: () => api.get('/api/auth/me'), acceptInvite: (body: AcceptInviteRequest) => api.post('/api/auth/accept-invite', body), + // 회원가입 — 성공 시 상태(승인 대기/활성) 반환. 스켈레톤이면 501(호출부 degraded). + register: (body: RegisterRequest) => + api.post('/api/auth/register', body, { anonymous: true }), + // 비밀번호 찾기(요청) — 사용자 열거 방지 위해 백엔드는 항상 성공 처리. + forgotPassword: (body: ForgotPasswordRequest) => + api.post('/api/auth/password/forgot', body, { anonymous: true }), + // 비밀번호 재설정 — 이메일+인증코드+새 비밀번호. + resetPassword: (body: ResetPasswordRequest) => + api.post('/api/auth/password/reset', body, { anonymous: true }), }; // ── M2 플로어플랜 (SCR-03/04) ── diff --git a/src/frontend/src/api/types.ts b/src/frontend/src/api/types.ts index 010a8a2..8edfb3e 100644 --- a/src/frontend/src/api/types.ts +++ b/src/frontend/src/api/types.ts @@ -68,6 +68,31 @@ export interface AcceptInviteRequest { inviteCode: string; companyRegistrationNo: string; } + +// 회원가입 (SCR-01 Signup) +export interface RegisterRequest { + email: string; + displayName: string; + password: string; + companyName?: string; + inviteCode?: string; +} +export interface RegisterResponse { + userId: string; + // 계정 상태 — 'ACTIVE'(즉시 로그인) | 'PENDING_APPROVAL'(승인 대기) 등 백엔드 권위 + status: string; + message?: string; +} + +// 비밀번호 찾기/초기화 (SCR-01 ForgotPassword 2단계) +export interface ForgotPasswordRequest { + email: string; +} +export interface ResetPasswordRequest { + email: string; + code: string; + newPassword: string; +} export interface KintexPrincipal { userId: string; displayName: string; diff --git a/src/frontend/src/components/ui/AiImage.tsx b/src/frontend/src/components/ui/AiImage.tsx index 290c7d6..0b2b786 100644 --- a/src/frontend/src/components/ui/AiImage.tsx +++ b/src/frontend/src/components/ui/AiImage.tsx @@ -15,6 +15,10 @@ interface AiImageProps { status?: RenderJobDto['status']; onRetry?: () => void; alt?: string; + /** 번들 예시(사용자 부스 실 렌더 아님)임을 표시 — "예시" 칩 노출. */ + sample?: boolean; + /** 예시 타일에서 "이 뷰 생성" 버튼 노출. */ + onGenerate?: () => void; } export function AiImage({ @@ -26,6 +30,8 @@ export function AiImage({ status = 'DONE', onRetry, alt = 'AI 생성 예상 이미지', + sample = false, + onGenerate, }: AiImageProps) { return (
@@ -55,9 +61,16 @@ export function AiImage({
AI 생성 이미지 + {sample && 예시} {shotLabel && {shotLabel}} {versionTag && {versionTag}}
+ + {sample && onGenerate && ( + + )} {/* 고지문 — 항상 노출 */}
{notice}
diff --git a/src/frontend/src/components/ui/ai-image.css b/src/frontend/src/components/ui/ai-image.css index f1d7a9e..f001fae 100644 --- a/src/frontend/src/components/ui/ai-image.css +++ b/src/frontend/src/components/ui/ai-image.css @@ -95,6 +95,34 @@ font-size: 11px; font-weight: 600; } +.kx-aiimg__sample { + background: rgba(109, 74, 255, 0.9); + color: var(--color-white); + border-radius: var(--radius-pill); + padding: 2px 8px; + font-size: 11px; + font-weight: 700; +} +.kx-aiimg__gen { + position: absolute; + bottom: var(--space-2); + right: var(--space-2); + display: inline-flex; + align-items: center; + gap: 4px; + background: var(--color-ai-accent); + color: var(--color-white); + border: none; + border-radius: var(--radius-sm); + padding: 6px 12px; + font-size: var(--fs-caption); + font-weight: 600; + cursor: pointer; + box-shadow: var(--shadow-level2); +} +.kx-aiimg__gen:hover { + background: #5b3aeb; +} .kx-aiimg__notice { font-size: 11px; line-height: 15px; diff --git a/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx b/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx new file mode 100644 index 0000000..d1ec0c5 --- /dev/null +++ b/src/frontend/src/screens/approval/ManagerApprovalQueuePage.tsx @@ -0,0 +1,184 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button } from '../../components/ui/Button'; +import { DdayChip } from '../../components/ui/Badge'; +import './approval-queue.css'; + +/* + * SCR-10 검수 대기열(홀매니저). Stitch manager_approval_queue 이식. + * - 홀 탭 + 요약 스탯(대기·오늘마감·AI 차단) + 검색 · 서류 테이블(AI 사전심사 배지) · 우: 홀 부하/AI 인사이트 + * ★ 계약에 문서 검수 큐 엔드포인트 부재 → 샘플 시연. '검수'→ SCR-11 실 라우트. + */ +interface DocRow { + id: string; + event: string; + booth: string; + docType: string; + submitter: string; + submittedAt: string; + dday: number; + ai: 'block' | 'pass'; + aiCount: number; + status: string; +} + +const ROWS: DocRow[] = [ + { id: 'd1', event: '스마트팩토리 코리아', booth: 'A-102', docType: '장치 도면', submitter: '(주)공간디자인', submittedAt: '07.10', dday: 6, ai: 'block', aiCount: 1, status: '검토 대기' }, + { id: 'd2', event: '스마트팩토리 코리아', booth: 'B-201', docType: '유틸리티 위치표시도', submitter: '미래테크(주)', submittedAt: '07.11', dday: 14, ai: 'pass', aiCount: 0, status: '검토 대기' }, + { id: 'd3', event: '스마트팩토리 코리아', booth: 'C-118', docType: '방염 확인서', submitter: '한빛로보틱스', submittedAt: '07.11', dday: 9, ai: 'pass', aiCount: 0, status: '검토 대기' }, + { id: 'd4', event: '스마트팩토리 코리아', booth: 'A-330', docType: '구조계산서', submitter: '(주)대한전시', submittedAt: '07.09', dday: 3, ai: 'block', aiCount: 2, status: '검토 대기' }, +]; + +const HALLS = ['홀6', '홀7', '홀8']; + +export function ManagerApprovalQueuePage() { + const { eventId = '', hallId = 'H7' } = useParams(); + const navigate = useNavigate(); + const [hall, setHall] = useState('홀7'); + + return ( +
+ {/* 서브 헤더 */} +
+
+
+

도면 및 서류 검수 대기열

+
+ {HALLS.map((h) => ( + + ))} +
+
+
+ + +
+
+
+
+ 검수 대기 + 23 +
+
+ 오늘 마감 + 5 +
+
+ ⚠ AI 차단 플래그 + 4 +
+
+
+ +
+ {/* 테이블 */} +
+
+

제출 서류 목록 ({hall})

+ +
+
+ + + + + + + + + + + + + + + + {ROWS.map((r) => ( + + + + + + + + + + + + ))} + +
행사/부스서류 유형제출자제출일마감AI 사전심사상태액션
+
{r.event}
+
{r.booth}
+
{r.docType}{r.submitter}{r.submittedAt} + {r.ai === 'block' ? ( + ⚠ 차단 {r.aiCount} + ) : ( + ✓ 통과 + )} + {r.status} + +
+
+
+ + {/* 우 사이드 */} + +
+
+ ); +} diff --git a/src/frontend/src/screens/approval/ReviewDetailsPage.tsx b/src/frontend/src/screens/approval/ReviewDetailsPage.tsx new file mode 100644 index 0000000..3d0172a --- /dev/null +++ b/src/frontend/src/screens/approval/ReviewDetailsPage.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button } from '../../components/ui/Button'; +import { AiLabel } from '../../components/ui/Badge'; +import './review-details.css'; + +/* + * SCR-11 검수 상세. Stitch review_details 이식. + * - 좌 70% 다크 캔버스(도면 + AI 위반 핀 오버레이, 탭·줌 컨트롤) / 우 30% AI 사전심사 결과 + 협의 이력 + * - 하단: 반려/승인 (모든 위반 '확인함' 체크 시 승인 활성) + * ★ 계약에 검수 상세 엔드포인트 부재 → 샘플. 승인/반려는 로컬 처리(백엔드 연동 준비). + */ +const MOCK = `${import.meta.env.BASE_URL}mock/review_details`; +const BLUEPRINT = `${MOCK}/img1.jpg`; + +interface V { + pin: number; + title: string; + detail: string; +} +const VIOLATIONS: V[] = [ + { pin: 1, title: '높이 규정 위반 (목공 벽체)', detail: '제출된 도면의 후면 벽체 높이가 4.5m로, 해당 구역 제한 높이(4.0m)를 초과함.' }, + { pin: 2, title: '통로 여유 공간 부족', detail: '우측 전시물 배치가 지정된 통로 경계선으로부터 500mm 여유 공간을 확보하지 않음.' }, + { pin: 3, title: '소화기 배치 위치 불명확', detail: '안전 규정에 따른 필수 소화기 비치 위치가 도면 상에 명시되지 않음.' }, +]; + +const TABS = ['도면', '예상 사진 (참고용)', '버전 비교']; +const PIN_POS = [ + { top: '30%', left: '45%' }, + { top: '60%', left: '20%' }, + { top: '50%', left: '68%' }, +]; + +export function ReviewDetailsPage() { + const { booth = 'A-102' } = useParams(); + const navigate = useNavigate(); + const [tab, setTab] = useState(0); + const [overlay, setOverlay] = useState(true); + const [checked, setChecked] = useState>({}); + const [decision, setDecision] = useState<'approved' | 'rejected' | null>(null); + + const allChecked = VIOLATIONS.every((v) => checked[v.pin]); + + return ( +
+ {/* 헤더 */} +
+
+ +
+

장치 도면 검토

+ 버전: v2 + 제출자: (주)공간디자인 + 일자: 07.10 +
+
+ +
+ +
+ {/* 캔버스 */} +
+
+
+ {TABS.map((t, i) => ( + + ))} +
+ +
+ +
+ +
+ + {/* 우 패널 */} + +
+ + {/* 하단 액션 */} +
+

+ {decision === 'approved' + ? '승인 처리되었습니다.' + : decision === 'rejected' + ? '반려 처리되었습니다 — 제출자에게 통보됩니다.' + : allChecked + ? '모든 위반 항목을 확인했습니다.' + : '위반 항목을 모두 확인해야 승인할 수 있습니다.'} +

+
+ + +
+
+
+ ); +} diff --git a/src/frontend/src/screens/approval/approval-queue.css b/src/frontend/src/screens/approval/approval-queue.css new file mode 100644 index 0000000..07cbaa4 --- /dev/null +++ b/src/frontend/src/screens/approval/approval-queue.css @@ -0,0 +1,290 @@ +.kx-aq { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} +.kx-aq__head { + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-4) var(--space-5); + border-bottom: var(--border-card); + background: var(--color-white); +} +.kx-aq__head-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); +} +.kx-aq__title-row { + display: flex; + align-items: center; + gap: var(--space-4); +} +.kx-aq__title-row h1 { + font-size: var(--fs-h3); + font-weight: 700; + color: var(--color-neutral-900); +} +.kx-aq__hall-tabs { + display: flex; + gap: var(--space-2); +} +.kx-aq__hall-tabs button { + padding: 4px 14px; + border: 1px solid var(--color-neutral-200); + background: var(--color-white); + border-radius: var(--radius-pill); + font-size: var(--fs-caption); + color: var(--color-neutral-500); + cursor: pointer; +} +.kx-aq__hall-tabs button.is-active { + border-color: var(--color-primary-600); + background: var(--color-primary-050); + color: var(--color-primary-700); + font-weight: 700; +} +.kx-aq__search { + display: flex; + align-items: center; + gap: var(--space-2); + border: 1px solid var(--color-neutral-200); + border-radius: var(--radius-sm); + padding: 6px 10px; + color: var(--color-neutral-500); +} +.kx-aq__search input { + border: none; + outline: none; + font-size: var(--fs-body); + width: 220px; +} +.kx-aq__stats { + display: flex; + gap: var(--space-3); +} +.kx-aq__stat { + display: flex; + align-items: center; + gap: var(--space-2); + border: 1px solid var(--color-neutral-200); + border-radius: var(--radius-lg); + padding: var(--space-2) var(--space-4); +} +.kx-aq__stat span { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-aq__stat strong { + font-size: var(--fs-h3); + color: var(--color-neutral-900); +} +.kx-aq__stat--primary { + color: var(--color-primary-600) !important; +} +.kx-aq__stat--alert { + background: #fef3f2; + border-color: #fecdca; +} +.kx-aq__stat--alert span, +.kx-aq__stat--alert strong { + color: var(--color-error); +} + +.kx-aq__body { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} +.kx-aq__main { + flex: 1; + display: flex; + flex-direction: column; + gap: var(--space-3); + padding: var(--space-4) var(--space-5); + overflow: auto; +} +.kx-aq__main-head { + display: flex; + align-items: center; + justify-content: space-between; +} +.kx-aq__main-head h2 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); +} +.kx-aq__table-wrap { + border: var(--border-card); + border-radius: var(--radius-lg); + overflow: auto; + background: var(--color-white); +} +.kx-aq__table { + width: 100%; + border-collapse: collapse; + font-size: var(--fs-body); +} +.kx-aq__table thead { + background: var(--color-primary-050); + position: sticky; + top: 0; +} +.kx-aq__table th { + text-align: left; + padding: var(--space-3); + font-size: var(--fs-caption); + color: var(--color-neutral-500); + font-weight: 600; + border-bottom: 1px solid var(--color-neutral-200); +} +.kx-aq__col-ai { + color: var(--color-ai-accent) !important; +} +.kx-aq__col-act { + text-align: right; +} +.kx-aq__table td { + padding: var(--space-3); + border-bottom: 1px solid var(--color-neutral-100); + vertical-align: middle; +} +.kx-aq__table tbody tr:hover { + background: var(--row-hover); +} +.kx-aq__event { + font-weight: 700; + color: var(--color-neutral-900); +} +.kx-aq__booth { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-aq__muted { + color: var(--color-neutral-500); +} +.kx-aq__ai { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + font-weight: 700; + padding: 3px 8px; + border-radius: var(--radius-sm); +} +.kx-aq__ai--block { + color: var(--color-error); + background: #fef3f2; + border: 1px solid #fecdca; +} +.kx-aq__ai--pass { + color: var(--color-success); + background: #e6f4ee; + border: 1px solid #a6e0c8; +} +.kx-aq__review { + border: 1px solid var(--color-neutral-200); + background: var(--color-white); + color: var(--color-primary-600); + border-radius: var(--radius-sm); + padding: 5px 14px; + font-size: var(--fs-caption); + font-weight: 600; + cursor: pointer; +} +.kx-aq__review:hover { + background: var(--color-primary-050); +} + +/* side */ +.kx-aq__side { + width: 320px; + border-left: var(--border-card); + background: var(--color-neutral-050); + padding: var(--space-5); + display: flex; + flex-direction: column; + gap: var(--space-4); + overflow-y: auto; +} +.kx-aq__card { + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-3); +} +.kx-aq__card h3 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); + border-bottom: 1px solid var(--color-neutral-100); + padding-bottom: var(--space-2); +} +.kx-aq__load { + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.kx-aq__load-top { + display: flex; + align-items: flex-end; + justify-content: space-between; +} +.kx-aq__load-top span { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-aq__load-top strong { + font-size: var(--fs-h3); + color: var(--color-neutral-900); +} +.kx-aq__bar { + height: 8px; + background: var(--color-neutral-100); + border-radius: var(--radius-pill); + overflow: hidden; +} +.kx-aq__bar-fill { + height: 100%; + background: var(--color-primary-600); +} +.kx-aq__bar-fill--green { + background: var(--color-success); +} +.kx-aq__load-scale { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--color-neutral-500); +} +.kx-aq__card--ai { + border-color: var(--color-ai-accent); + background: var(--color-ai-surface); +} +.kx-aq__ai-head { + display: flex; + align-items: center; + gap: var(--space-2); + color: var(--color-ai-accent); +} +.kx-aq__ai-head h3 { + border: none; + padding: 0; + font-size: var(--fs-h3); +} +.kx-aq__card--ai p { + font-size: var(--fs-body); + line-height: var(--lh-body); + color: var(--color-neutral-700); +} + +@media (max-width: 1100px) { + .kx-aq__side { + display: none; + } +} diff --git a/src/frontend/src/screens/approval/review-details.css b/src/frontend/src/screens/approval/review-details.css new file mode 100644 index 0000000..9e94d3c --- /dev/null +++ b/src/frontend/src/screens/approval/review-details.css @@ -0,0 +1,369 @@ +.kx-rev { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} +.kx-rev__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-3) var(--space-5); + border-bottom: var(--border-card); + background: var(--color-white); +} +.kx-rev__crumb { + display: flex; + align-items: center; + gap: var(--space-2); + font-size: var(--fs-caption); + color: var(--color-neutral-500); + margin-bottom: var(--space-1); +} +.kx-rev__crumb button { + border: none; + background: none; + color: var(--color-neutral-500); + cursor: pointer; + font-size: var(--fs-caption); + padding: 0; +} +.kx-rev__crumb button:hover { + color: var(--color-primary-600); +} +.kx-rev__crumb-cur { + color: var(--color-neutral-900); +} +.kx-rev__title-row { + display: flex; + align-items: center; + gap: var(--space-2); +} +.kx-rev__title-row h1 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); +} +.kx-rev__meta { + font-size: 11px; + color: var(--color-neutral-500); + background: var(--color-neutral-100); + border: 1px solid var(--color-neutral-200); + padding: 2px 8px; + border-radius: var(--radius-sm); +} + +.kx-rev__body { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* 캔버스 */ +.kx-rev__canvas { + flex: 7; + position: relative; + display: flex; + flex-direction: column; + background: var(--color-canvas-bg); + border-right: var(--border-card); +} +.kx-rev__canvas-top { + position: absolute; + top: var(--space-4); + left: var(--space-4); + right: var(--space-4); + z-index: 2; + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-3); + pointer-events: none; +} +.kx-rev__tabs { + display: flex; + gap: 2px; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-sm); + padding: 3px; + pointer-events: auto; +} +.kx-rev__tabs button { + border: none; + background: transparent; + color: rgba(255, 255, 255, 0.7); + padding: 6px 14px; + border-radius: var(--radius-sm); + font-size: var(--fs-caption); + cursor: pointer; +} +.kx-rev__tabs button.is-active { + background: rgba(0, 102, 179, 0.35); + color: var(--color-white); + border: 1px solid rgba(0, 102, 179, 0.5); +} +.kx-rev__overlay-toggle { + display: flex; + align-items: center; + gap: var(--space-2); + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px); + border: 1px solid var(--color-ai-accent); + color: var(--color-white); + border-radius: var(--radius-sm); + padding: 7px 12px; + font-size: var(--fs-caption); + cursor: pointer; + pointer-events: auto; + box-shadow: 0 0 15px rgba(109, 74, 255, 0.2); +} +.kx-rev__switch { + width: 30px; + height: 16px; + border-radius: var(--radius-pill); + background: rgba(255, 255, 255, 0.25); + position: relative; +} +.kx-rev__overlay-toggle.is-on .kx-rev__switch { + background: var(--color-ai-accent); +} +.kx-rev__switch-knob { + position: absolute; + top: 2px; + left: 2px; + width: 12px; + height: 12px; + border-radius: 50%; + background: var(--color-white); + transition: left 0.15s; +} +.kx-rev__overlay-toggle.is-on .kx-rev__switch-knob { + left: 16px; +} + +.kx-rev__viewer { + flex: 1; + position: relative; + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} +.kx-rev__grid-bg { + position: absolute; + inset: 0; + opacity: 0.2; + background-image: radial-gradient(circle at 2px 2px, white 1px, transparent 0); + background-size: 40px 40px; +} +.kx-rev__drawing { + position: relative; + width: 78%; + height: 78%; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: var(--radius-sm); + overflow: hidden; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); +} +.kx-rev__drawing img { + width: 100%; + height: 100%; + object-fit: cover; + opacity: 0.75; + mix-blend-mode: screen; +} +.kx-rev__pin { + position: absolute; + width: 30px; + height: 30px; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--color-error); + color: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: var(--fs-caption); + border: 2px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} +.kx-rev__zoom { + position: absolute; + bottom: var(--space-4); + right: var(--space-4); + display: flex; + flex-direction: column; + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(8px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-sm); + overflow: hidden; +} +.kx-rev__zoom button { + border: none; + background: transparent; + color: var(--color-white); + padding: 8px 10px; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + font-size: 15px; +} +.kx-rev__zoom button:last-child { + border-bottom: none; +} +.kx-rev__zoom button:hover { + background: rgba(255, 255, 255, 0.12); +} + +/* 우 패널 */ +.kx-rev__panel { + flex: 3; + min-width: 300px; + background: var(--color-white); + overflow-y: auto; + padding: var(--space-4); + display: flex; + flex-direction: column; + gap: var(--space-5); +} +.kx-rev__ai-card { + border: var(--border-card); + border-radius: var(--radius-lg); + overflow: hidden; +} +.kx-rev__ai-card-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3); + background: var(--color-ai-surface); + border-bottom: 1px solid var(--color-neutral-200); +} +.kx-rev__ai-count { + font-size: 11px; + font-weight: 700; + color: var(--color-error); + background: #fef3f2; + padding: 2px 8px; + border-radius: var(--radius-sm); +} +.kx-rev__vlist { + list-style: none; + padding: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-3); +} +.kx-rev__vlist li { + display: flex; + gap: var(--space-3); + padding-bottom: var(--space-3); + border-bottom: 1px solid var(--color-neutral-100); +} +.kx-rev__vlist li:last-child { + border-bottom: none; + padding-bottom: 0; +} +.kx-rev__vpin { + width: 24px; + height: 24px; + border-radius: 50%; + background: #fef3f2; + color: var(--color-error); + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + font-size: 12px; + flex-shrink: 0; +} +.kx-rev__vtitle { + font-size: var(--fs-body); + font-weight: 600; + color: var(--color-neutral-900); + margin-bottom: 2px; +} +.kx-rev__vdetail { + font-size: var(--fs-caption); + color: var(--color-neutral-500); + margin-bottom: var(--space-2); + line-height: 16px; +} +.kx-rev__vcheck { + display: inline-flex; + align-items: center; + gap: var(--space-2); + font-size: var(--fs-caption); + color: var(--color-neutral-700); + cursor: pointer; +} + +.kx-rev__thread h3 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); + margin-bottom: var(--space-3); +} +.kx-rev__comment { + display: flex; + gap: var(--space-3); + margin-bottom: var(--space-3); +} +.kx-rev__avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: var(--color-primary-100); + color: var(--color-primary-700); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 700; + flex-shrink: 0; +} +.kx-rev__avatar--con { + background: var(--color-ai-surface); + color: var(--color-ai-accent); +} +.kx-rev__comment-top { + display: flex; + align-items: baseline; + gap: var(--space-2); + margin-bottom: 2px; +} +.kx-rev__comment-top strong { + font-size: var(--fs-caption); + color: var(--color-neutral-900); +} +.kx-rev__comment-top span { + font-size: 11px; + color: var(--color-neutral-500); +} +.kx-rev__comment p { + font-size: var(--fs-caption); + color: var(--color-neutral-700); + line-height: 16px; +} + +/* 하단 */ +.kx-rev__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-3) var(--space-5); + border-top: var(--border-card); + background: var(--color-white); +} +.kx-rev__foot-note { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-rev__foot-actions { + display: flex; + gap: var(--space-3); +} diff --git a/src/frontend/src/screens/compliance/ComplianceReportPage.tsx b/src/frontend/src/screens/compliance/ComplianceReportPage.tsx new file mode 100644 index 0000000..d163fe2 --- /dev/null +++ b/src/frontend/src/screens/compliance/ComplianceReportPage.tsx @@ -0,0 +1,139 @@ +import { useParams } from 'react-router-dom'; +import { Button } from '../../components/ui/Button'; +import { AiLabel } from '../../components/ui/Badge'; +import type { ComplianceReport, Violation } from '../../api/types'; +import './compliance-report.css'; + +/* + * SCR-09 규정 검증 결과. Stitch compliance_report 이식. + * - 상단 요약 배너(차단/경고/통과) — 차단 존재 시 제출 불가 + * - 좌: 자동 검증 체크리스트(그룹별) / 우: 상세 도면 검토(Elevation) + 위반 오버레이 핀 + * ★ 계약 §4 precheck(ComplianceReport) shape 사용 — 여기선 확정 리포트 시연(샘플). + */ +const MOCK = `${import.meta.env.BASE_URL}mock/compliance_report`; +const ELEVATION = `${MOCK}/img1.jpg`; + +const SAMPLE_REPORT: ComplianceReport = { + rulesetVersion: 'v1.0', + disclaimer: '본 검증은 사전 심사이며 최종 승인은 현장 검수·구조기술사 판단이 우선합니다.', + blockCount: 1, + warnCount: 2, + passCount: 14, + submittable: false, + violations: [ + { pin: 1, code: 'H-01', group: '높이 규정', label: '장치물 높이 5m 이하', severity: 'block', measured: '5.4m (제한 5m)' }, + { pin: 2, code: 'R-03', group: '리깅', label: '리깅 트러스 6.5m 초과 구조계산', severity: 'warn', measured: '7.0m', requiresDocument: '구조계산서 D-7' }, + { pin: 3, code: 'A-02', group: '통로', label: '통로 여유 공간 확보', severity: 'warn', measured: '통로 인접 80% 여유' }, + ], +}; + +const PASS_GROUPS = [ + { group: '퇴출구', label: '퇴출구 확보' }, + { group: '방염', label: '방염 자재 사용 확인' }, + { group: '전기', label: '전력 부하 적정' }, + { group: '표시', label: '통로 표시 도장 적정' }, +]; + +const SEV_META: Record = { + block: { label: '차단', cls: 'block' }, + warn: { label: '경고', cls: 'warn' }, +}; + +export function ComplianceReportPage() { + const { boothId = 'A-102' } = useParams(); + const report = SAMPLE_REPORT; + + return ( +
+
+

부스 {boothId} · 설계 v2 · 규정 검증 결과

+
+ + {/* 요약 배너 */} +
+ + + 차단 {report.blockCount}건 · 경고 {report.warnCount}건 · 통과 {report.passCount}건 + + {!report.submittable && 차단 항목 해결 전 제출 불가} +
+ + {/* PDF 업로드 존 */} +
+ +

기존 도면 업로드 (PDF/이미지)

+ 드래그 앤 드롭 또는 클릭해 파일 선택 + AI 자동 검증 +
+ +
+ {/* 체크리스트 */} +
+
+

자동 검증 체크리스트

+
+ 통과 + 경고 + 차단 +
+
+ +
    + {report.violations.map((v) => ( +
  • + +
    +
    + {v.label} + + {SEV_META[v.severity].label} + +
    + + {v.group} · 측정 {v.measured} + + {v.requiresDocument && ( + {v.requiresDocument} 제출 필요 + )} +
    +
  • + ))} + {PASS_GROUPS.map((p) => ( +
  • + +
    +
    + {p.label} + 통과 +
    +
    +
  • + ))} +
+
+ + {/* 상세 도면 검토 */} +
+
+

상세 도면 검토 (Elevation)

+
+
+ 상세 도면 (입면) 검토 +
+
+
+ +
+

{report.disclaimer} · 룰셋 {report.rulesetVersion}

+ +
+
+ ); +} diff --git a/src/frontend/src/screens/compliance/compliance-report.css b/src/frontend/src/screens/compliance/compliance-report.css new file mode 100644 index 0000000..4489efd --- /dev/null +++ b/src/frontend/src/screens/compliance/compliance-report.css @@ -0,0 +1,257 @@ +.kx-comp { + display: flex; + flex-direction: column; + gap: var(--space-4); + padding: var(--space-5); + max-width: var(--container-max); +} +.kx-comp__head h1 { + font-size: var(--fs-h1); + color: var(--color-neutral-900); +} + +.kx-comp__banner { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-3) var(--space-4); + border-radius: var(--radius-lg); + font-size: var(--fs-body); + font-weight: 600; +} +.kx-comp__banner.is-block { + background: #fef3f2; + color: var(--color-error); + border: 1px solid #fecdca; +} +.kx-comp__banner.is-ok { + background: #e6f4ee; + color: var(--color-success); + border: 1px solid #a6e0c8; +} +.kx-comp__banner-note { + font-weight: 400; +} + +.kx-comp__drop { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + border: 1px dashed var(--color-neutral-200); + border-radius: var(--radius-lg); + background: var(--color-neutral-050); + padding: var(--space-6); + color: var(--color-neutral-500); + font-size: var(--fs-body); +} +.kx-comp__drop-icon { + font-size: 22px; + color: var(--color-ai-accent); +} +.kx-comp__drop-hint { + font-size: var(--fs-caption); +} + +.kx-comp__body { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-4); + align-items: start; +} + +.kx-comp__list { + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); + padding: var(--space-4); +} +.kx-comp__list-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: var(--space-3); +} +.kx-comp__list-head h2 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); +} +.kx-comp__legend { + display: flex; + gap: var(--space-2); +} +.kx-comp__lg { + font-size: 11px; + font-weight: 600; + padding: 1px 8px; + border-radius: var(--radius-pill); +} +.kx-comp__lg--pass { + background: #e6f4ee; + color: var(--color-success); +} +.kx-comp__lg--warn { + background: var(--color-violation-warn-bg); + color: var(--color-violation-warn-text); +} +.kx-comp__lg--block { + background: #fef3f2; + color: var(--color-error); +} + +.kx-comp__items { + list-style: none; + display: flex; + flex-direction: column; + gap: var(--space-2); +} +.kx-comp__item { + display: flex; + gap: var(--space-3); + padding: var(--space-3); + border-radius: var(--radius-sm); + border: 1px solid var(--color-neutral-200); +} +.kx-comp__item.is-block { + border-color: #fecdca; + background: #fffbfa; +} +.kx-comp__item.is-warn { + border-color: #fde3ad; + background: #fffcf5; +} +.kx-comp__item.is-pass { + border-color: var(--color-neutral-100); +} +.kx-comp__pin { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 700; + flex-shrink: 0; + background: var(--color-error); + color: var(--color-white); +} +.kx-comp__item.is-warn .kx-comp__pin { + background: var(--color-violation-warn); +} +.kx-comp__pin--pass { + background: var(--color-success) !important; +} +.kx-comp__item-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} +.kx-comp__item-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-2); +} +.kx-comp__item-label { + font-size: var(--fs-body); + font-weight: 600; + color: var(--color-neutral-900); +} +.kx-comp__sev { + font-size: 11px; + font-weight: 700; + padding: 1px 8px; + border-radius: var(--radius-pill); +} +.kx-comp__sev--block { + background: #fef3f2; + color: var(--color-error); +} +.kx-comp__sev--warn { + background: var(--color-violation-warn-bg); + color: var(--color-violation-warn-text); +} +.kx-comp__sev--pass { + background: #e6f4ee; + color: var(--color-success); +} +.kx-comp__measured { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} +.kx-comp__doc { + font-size: 11px; + font-weight: 600; + color: var(--color-ai-accent); + background: var(--color-ai-surface); + border: 1px solid var(--color-ai-accent); + border-radius: var(--radius-sm); + padding: 2px 8px; + align-self: flex-start; +} + +/* 도면 */ +.kx-comp__drawing { + background: var(--color-white); + border: var(--border-card); + border-radius: var(--radius-lg); + padding: var(--space-4); +} +.kx-comp__drawing-head h2 { + font-size: var(--fs-h3); + color: var(--color-neutral-900); + margin-bottom: var(--space-3); +} +.kx-comp__drawing-frame { + position: relative; + margin: 0; + border-radius: var(--radius-lg); + overflow: hidden; + border: var(--border-card); + background: #f6f8fb; +} +.kx-comp__drawing-frame img { + width: 100%; + display: block; + object-fit: cover; +} +.kx-comp__overlay-box { + position: absolute; + top: 12%; + left: 30%; + width: 40%; + height: 22%; + border: 2px solid var(--color-error); + background: rgba(217, 45, 32, 0.15); + border-radius: 2px; +} +.kx-comp__overlay-badge { + position: absolute; + top: 12%; + right: 6%; + font-size: 11px; + font-weight: 700; + color: var(--color-white); + background: var(--color-error); + padding: 3px 8px; + border-radius: var(--radius-sm); +} + +.kx-comp__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); +} +.kx-comp__disclaimer { + font-size: var(--fs-caption); + color: var(--color-neutral-500); +} + +@media (max-width: 1000px) { + .kx-comp__body { + grid-template-columns: 1fr; + } +} diff --git a/src/frontend/src/screens/dashboard/OrganizerDashboardPage.tsx b/src/frontend/src/screens/dashboard/OrganizerDashboardPage.tsx index ba24129..56dc919 100644 --- a/src/frontend/src/screens/dashboard/OrganizerDashboardPage.tsx +++ b/src/frontend/src/screens/dashboard/OrganizerDashboardPage.tsx @@ -11,6 +11,11 @@ import { } from './sampleDashboard'; import './dashboard.css'; +// Stitch organizer_dashboard 번들 이미지(실 KINTEX 촬영본). +const MOCK = `${import.meta.env.BASE_URL}mock/organizer_dashboard`; +const S7_AERIAL = `${MOCK}/img3.jpg`; // 홀 전경 조감(아이소메트릭 항공뷰) +const BOOTH_THUMBS = [`${MOCK}/img1.jpg`, `${MOCK}/img2.jpg`, `${MOCK}/img3.jpg`]; + /* * SCR-02 주최자 대시보드. Stitch organizer_dashboard 이식. * - 히어로: D-데이 마일스톤 타임라인 + KPI 4개 + 참가업체 테이블 + 알림 피드 + S7 미니 카드. @@ -100,7 +105,7 @@ export function OrganizerDashboardPage() { - {SAMPLE_EXHIBITORS.map((r) => ( + {SAMPLE_EXHIBITORS.map((r, i) => ( {r.boothNo} {r.company} @@ -111,7 +116,12 @@ export function OrganizerDashboardPage() { -