- 채팅 링크로 바로 진입하면 세션이 협상생성(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
80 lines
2.6 KiB
TypeScript
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
|