o2o-negosium-original/frontend/src/layouts/MainLayout.tsx
Mina Choi db07a60cfa [fix] negosium·negodata·landing: 채팅 진입·부가정보 저장 오류 수정, 모바일 여백·스크롤·노치 대응
- 채팅 링크로 바로 진입하면 세션이 협상생성(1)에 머물러 오프닝 메시지가 seed 되지 않던 문제:
  chat init/messages 에서 participate 와 동일하게 협상중(2)으로 전이(마감 견적·마감시간 초과는 제외)
- 부가정보 저장이 1301 로 실패하던 문제: '협상완료' 요약은 chat_end=false 라 세션이 아직 협상중이므로,
  마지막 말풍선이 summaryRSP/CM 이면 협상중 세션도 저장 허용
- 채팅 언마운트 시 messages 캐시도 제거 — staleTime:Infinity 라 빈 목록이 남아 재진입해도 빈 화면이던 문제
- 채팅 스크롤을 컨테이너 기준 scrollTo 로 변경(rAF 후 실행) — 요약·폼이 뒤늦게 레이아웃되면 중간에 멈추던 문제
- 모바일 채팅 상단 여백 제거(pt-[64px] 는 우측 협상절차 카드가 있는 lg 이상만)
- 노치 대응: viewport-fit=cover + safe-t/b/x 유틸 신설, 헤더·하단 액션 덱·드로어·랜딩 헤더에 적용, h-screen→100dvh
2026-07-23 12:50:40 +09:00

80 lines
2.6 KiB
TypeScript

import { type ReactNode } from 'react'
import { Logo } from '@/components'
import { useMeQuery } from '@/apis'
import { cn } from '@/lib'
// 좌측 폭: list=반응형 비율 / chat=고정폭 단계 축소
const SIDEBAR_WIDTH = {
list:
'w-[20%] max-w-[320px] ' +
'max-[1520px]:w-[23%] max-[1410px]:w-[25%] ' +
'max-[1180px]:w-[27%] max-[1024px]:w-[29%] max-[960px]:w-[33%]',
chat: 'w-[300px] max-[1350px]:w-[284px] max-[1180px]:w-[260px]',
} as const
const styles = {
root: 'flex w-full min-h-[100dvh] max-h-[100dvh] bg-background safe-x',
scrollX: 'flex w-full min-h-[100dvh] max-h-[100dvh] overflow-x-auto overflow-y-hidden safe-x',
scrollXInner: 'flex w-full min-h-[100dvh] max-h-[100dvh] min-w-[1024px] bg-background',
// 좌측 레일은 lg 미만에서 숨기고(드로어로 대체), main 이 전체 폭을 차지한다. 보고서식 풀블리드 + 경계선.
sidebar: 'hidden lg:flex flex-col h-[100dvh] bg-white border-r border-border',
logoHeader:
'flex w-full h-[56px] pl-[24px] pr-[16px] items-center justify-between shrink-0 border-b border-border',
main: 'flex flex-1 flex-col h-[100dvh] overflow-hidden',
content: 'flex flex-1 flex-col min-h-0 w-full overflow-hidden bg-surface',
} as const
export type SidebarWidth = keyof typeof SIDEBAR_WIDTH
export interface MainLayoutProps {
sidebarWidth?: SidebarWidth
/** 루트 가로 스크롤 + 최소폭 (chat 전용) */
scrollX?: boolean
logoAction?: ReactNode
sidebar: ReactNode
header: ReactNode
children?: ReactNode
}
export function MainLayout({
sidebarWidth = 'list',
scrollX = false,
logoAction,
sidebar,
header,
children,
}: MainLayoutProps) {
const { data: user } = useMeQuery() // 회사 브랜딩(서비스명/로고) 주입
const panes = (
<>
<aside className={cn(styles.sidebar, SIDEBAR_WIDTH[sidebarWidth])}>
<div className={styles.logoHeader}>
<Logo size="sm" serviceName={user?.branding?.service_name} logoUrl={user?.branding?.logo_url} />
{logoAction}
</div>
{sidebar}
<p className="mt-auto px-4 py-3 text-[10px] font-medium text-neutral-50">
© {new Date().getFullYear()} negotium · Made by AI O2O
</p>
</aside>
<main className={styles.main}>
{header}
<div className={styles.content}>{children}</div>
</main>
</>
)
if (!scrollX) {
return <div className={styles.root}>{panes}</div>
}
return (
<div className={styles.scrollX}>
<div className={styles.scrollXInner}>{panes}</div>
</div>
)
}
export default MainLayout