[feat] front: List/Chat 레이아웃 + 목록 필터 섹션 구현
- layouts: 공통 2분할 레이아웃 MainLayout(좌 사이드바/우 메인, 로고 헤더, 헤더 아래 콘텐츠 면) + 다크 헤더 바 MainHeaderBar 분리 - pages: ListPage/ChatPage 를 MainLayout 기반으로 구성(견적 선택/협상 잔여 시간 헤더) - features/list: 필터 섹션 추가 — ListFilter(요소)/FilterSection(컨테이너)/ filterOptions(데이터)/useListFilterStore(상태), SidebarFooter 분리 - deps: lucide-react 추가(필터 화살표·목록 이동 아이콘) - refactor: 모든 import 를 @ alias 로 통일 - fix: Button secondary variant hover(=accent 동일색) 및 필터 화살표 hover 색상 보정 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9de140abd6
commit
6da8976a20
10
front/package-lock.json
generated
10
front/package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"axios": "^1.18.0",
|
"axios": "^1.18.0",
|
||||||
|
"lucide-react": "^1.18.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-router": "^7.17.0",
|
"react-router": "^7.17.0",
|
||||||
@ -2816,6 +2817,15 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "1.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.18.0.tgz",
|
||||||
|
"integrity": "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.101.0",
|
"@tanstack/react-query": "^5.101.0",
|
||||||
"axios": "^1.18.0",
|
"axios": "^1.18.0",
|
||||||
|
"lucide-react": "^1.18.0",
|
||||||
"react": "^19.2.6",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.6",
|
"react-dom": "^19.2.6",
|
||||||
"react-router": "^7.17.0",
|
"react-router": "^7.17.0",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type ComponentProps } from 'react'
|
import { type ComponentProps } from 'react'
|
||||||
import { cn } from './cn'
|
import { cn } from '@/components/cn'
|
||||||
|
|
||||||
export type ButtonVariant =
|
export type ButtonVariant =
|
||||||
| 'primary'
|
| 'primary'
|
||||||
@ -19,7 +19,7 @@ const base =
|
|||||||
// hover 는 한 단계 톤다운(brand-700) — 디자인 선호
|
// hover 는 한 단계 톤다운(brand-700) — 디자인 선호
|
||||||
const variantClass: Record<ButtonVariant, string> = {
|
const variantClass: Record<ButtonVariant, string> = {
|
||||||
primary: 'bg-primary text-primary-foreground hover:bg-brand-700',
|
primary: 'bg-primary text-primary-foreground hover:bg-brand-700',
|
||||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-accent',
|
secondary: 'bg-secondary text-secondary-foreground hover:bg-border',
|
||||||
outline:
|
outline:
|
||||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type ComponentProps } from 'react'
|
import { type ComponentProps } from 'react'
|
||||||
import { cn } from './cn'
|
import { cn } from '@/components/cn'
|
||||||
|
|
||||||
export function Input({ type = 'text', className, ...props }: ComponentProps<'input'>) {
|
export function Input({ type = 'text', className, ...props }: ComponentProps<'input'>) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { type ComponentProps } from 'react'
|
import { type ComponentProps } from 'react'
|
||||||
import { cn } from './cn'
|
import { cn } from '@/components/cn'
|
||||||
import logoColor from '@/assets/imarketkorea-logo.png'
|
import logoColor from '@/assets/imarketkorea-logo.png'
|
||||||
import logoWhite from '@/assets/imarketkorea-logo-white.png'
|
import logoWhite from '@/assets/imarketkorea-logo-white.png'
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export { cn } from './cn'
|
export { cn } from '@/components/cn'
|
||||||
export { Button } from './Button'
|
export { Button } from '@/components/Button'
|
||||||
export type { ButtonProps, ButtonVariant, ButtonSize } from './Button'
|
export type { ButtonProps, ButtonVariant, ButtonSize } from '@/components/Button'
|
||||||
export { Input } from './Input'
|
export { Input } from '@/components/Input'
|
||||||
export { Logo } from './Logo'
|
export { Logo } from '@/components/Logo'
|
||||||
export type { LogoProps, LogoVariant } from './Logo'
|
export type { LogoProps, LogoVariant } from '@/components/Logo'
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { LoginForm } from './components/LoginForm'
|
export { LoginForm } from '@/features/auth/components/LoginForm'
|
||||||
|
|||||||
69
front/src/features/list/components/ListFilter.tsx
Normal file
69
front/src/features/list/components/ListFilter.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { ChevronDown } from 'lucide-react'
|
||||||
|
import { cn } from '@/components'
|
||||||
|
|
||||||
|
export interface ListFilterProps {
|
||||||
|
title: string
|
||||||
|
items: readonly string[]
|
||||||
|
selectedItem: string | null
|
||||||
|
onItemClick: (item: string) => void
|
||||||
|
defaultOpen?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ListFilter({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
selectedItem,
|
||||||
|
onItemClick,
|
||||||
|
defaultOpen = true,
|
||||||
|
}: ListFilterProps) {
|
||||||
|
const [open, setOpen] = useState(defaultOpen)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="flex justify-between items-center h-12 pl-3 pr-1">
|
||||||
|
<h3 className="text-2xl font-bold leading-[30px] text-foreground whitespace-nowrap">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
className="w-10 h-10 flex items-center justify-center cursor-pointer text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronDown
|
||||||
|
size={24}
|
||||||
|
className={cn('transition-transform duration-300', !open && 'rotate-180')}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* grid-rows 0fr→1fr 로 높이를 부드럽게 펼침 */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid transition-[grid-template-rows,opacity] duration-300 ease-out',
|
||||||
|
open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onItemClick(item)}
|
||||||
|
className={cn(
|
||||||
|
'h-11 px-3 flex items-center rounded-lg text-left text-base text-foreground',
|
||||||
|
'whitespace-nowrap cursor-pointer transition-colors duration-200',
|
||||||
|
selectedItem === item ? 'bg-secondary' : 'hover:bg-secondary/60',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
front/src/features/list/components/SidebarFooter.tsx
Normal file
21
front/src/features/list/components/SidebarFooter.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Button } from '@/components'
|
||||||
|
|
||||||
|
/** 좌측 사이드바 하단: 공급사명 + 로그아웃 */
|
||||||
|
export function SidebarFooter() {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full h-[60px] py-3 px-8 gap-3 items-center">
|
||||||
|
<div className="flex-1 flex items-center min-w-0 text-sm text-foreground">
|
||||||
|
{/* TODO: 공급사명 (auth store 연동) */}
|
||||||
|
<span className="truncate">-</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="w-[61px] px-2 rounded-[4px] text-xs flex-shrink-0"
|
||||||
|
// TODO: 로그아웃 mutation 연동
|
||||||
|
>
|
||||||
|
로그아웃
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
35
front/src/features/list/containers/FilterSection.tsx
Normal file
35
front/src/features/list/containers/FilterSection.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { ListFilter } from '@/features/list/components/ListFilter'
|
||||||
|
import { FILTER_GROUPS, type FilterKey } from '@/features/list/model/filterOptions'
|
||||||
|
import { useListFilterStore } from '@/features/list/model/useListFilterStore'
|
||||||
|
|
||||||
|
export function FilterSection() {
|
||||||
|
const store = useListFilterStore()
|
||||||
|
|
||||||
|
// 필터 키 → store 상태/핸들러 매핑
|
||||||
|
const selected: Record<FilterKey, string | null> = {
|
||||||
|
type: store.selectedType,
|
||||||
|
status: store.selectedStatus,
|
||||||
|
deadline: store.selectedDeadline,
|
||||||
|
}
|
||||||
|
const toggle: Record<FilterKey, (item: string) => void> = {
|
||||||
|
type: store.toggleType,
|
||||||
|
status: store.toggleStatus,
|
||||||
|
deadline: store.toggleDeadline,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 pt-8 pr-2 pl-5 overflow-hidden bg-background">
|
||||||
|
<div className="flex flex-col gap-4 pr-2 w-full overflow-y-auto">
|
||||||
|
{FILTER_GROUPS.map((group) => (
|
||||||
|
<ListFilter
|
||||||
|
key={group.key}
|
||||||
|
title={group.title}
|
||||||
|
items={group.items}
|
||||||
|
selectedItem={selected[group.key]}
|
||||||
|
onItemClick={toggle[group.key]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
4
front/src/features/list/index.ts
Normal file
4
front/src/features/list/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// 공개 API: 페이지에서 쓰는 것만 노출 (ListFilter/FILTER_GROUPS 등은 feature 내부 구현)
|
||||||
|
export { FilterSection } from '@/features/list/containers/FilterSection'
|
||||||
|
export { SidebarFooter } from '@/features/list/components/SidebarFooter'
|
||||||
|
export { useListFilterStore } from '@/features/list/model/useListFilterStore'
|
||||||
17
front/src/features/list/model/filterOptions.ts
Normal file
17
front/src/features/list/model/filterOptions.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export type FilterKey = 'type' | 'status' | 'deadline'
|
||||||
|
|
||||||
|
export interface FilterGroup {
|
||||||
|
key: FilterKey
|
||||||
|
title: string
|
||||||
|
items: readonly string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FILTER_GROUPS: readonly FilterGroup[] = [
|
||||||
|
{ key: 'type', title: '구분', items: ['재견적', '재협상'] },
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
title: '상태',
|
||||||
|
items: ['협상생성', '협상중', '협상완료', '미참여', '협상거부'],
|
||||||
|
},
|
||||||
|
{ key: 'deadline', title: '마감일', items: ['남은 시간 적은 순', '남은 시간 긴 순'] },
|
||||||
|
]
|
||||||
34
front/src/features/list/model/useListFilterStore.ts
Normal file
34
front/src/features/list/model/useListFilterStore.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { create } from 'zustand'
|
||||||
|
|
||||||
|
interface ListFilterState {
|
||||||
|
selectedType: string | null
|
||||||
|
selectedStatus: string | null
|
||||||
|
selectedDeadline: string | null
|
||||||
|
currentPage: number
|
||||||
|
|
||||||
|
toggleType: (item: string) => void
|
||||||
|
toggleStatus: (item: string) => void
|
||||||
|
toggleDeadline: (item: string) => void
|
||||||
|
setCurrentPage: (page: number) => void
|
||||||
|
resetFilters: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// 같은 항목을 다시 누르면 해제
|
||||||
|
const toggle = (current: string | null, item: string) => (current === item ? null : item)
|
||||||
|
|
||||||
|
export const useListFilterStore = create<ListFilterState>((set) => ({
|
||||||
|
selectedType: null,
|
||||||
|
selectedStatus: null,
|
||||||
|
selectedDeadline: null,
|
||||||
|
currentPage: 1,
|
||||||
|
|
||||||
|
toggleType: (item) =>
|
||||||
|
set((s) => ({ selectedType: toggle(s.selectedType, item), currentPage: 1 })),
|
||||||
|
toggleStatus: (item) =>
|
||||||
|
set((s) => ({ selectedStatus: toggle(s.selectedStatus, item), currentPage: 1 })),
|
||||||
|
toggleDeadline: (item) =>
|
||||||
|
set((s) => ({ selectedDeadline: toggle(s.selectedDeadline, item), currentPage: 1 })),
|
||||||
|
setCurrentPage: (page) => set({ currentPage: page }),
|
||||||
|
resetFilters: () =>
|
||||||
|
set({ selectedType: null, selectedStatus: null, selectedDeadline: null, currentPage: 1 }),
|
||||||
|
}))
|
||||||
23
front/src/layouts/MainHeaderBar.tsx
Normal file
23
front/src/layouts/MainHeaderBar.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { type ReactNode } from 'react'
|
||||||
|
import { cn } from '@/components'
|
||||||
|
|
||||||
|
// 우측 상단 다크 헤더 바
|
||||||
|
const HEADER_BASE =
|
||||||
|
'flex w-full h-[56px] py-[18px] items-center rounded-t-[8px] bg-foreground text-background'
|
||||||
|
|
||||||
|
const HEADER_ALIGN = {
|
||||||
|
left: 'justify-start',
|
||||||
|
center: 'justify-center',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export interface MainHeaderBarProps {
|
||||||
|
align?: keyof typeof HEADER_ALIGN
|
||||||
|
className?: string
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MainHeaderBar({ align = 'left', className, children }: MainHeaderBarProps) {
|
||||||
|
return <div className={cn(HEADER_BASE, HEADER_ALIGN[align], className)}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MainHeaderBar
|
||||||
74
front/src/layouts/MainLayout.tsx
Normal file
74
front/src/layouts/MainLayout.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { type ReactNode } from 'react'
|
||||||
|
import { Logo, cn } from '@/components'
|
||||||
|
|
||||||
|
// 좌측 폭: list=반응형 비율 / chat=고정 350px
|
||||||
|
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-[350px]',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
root: 'flex w-full min-h-screen max-h-screen bg-background',
|
||||||
|
scrollX: 'flex w-full min-h-screen max-h-screen overflow-x-auto overflow-y-hidden',
|
||||||
|
scrollXInner: 'flex w-full min-h-screen max-h-screen min-w-[1350px] bg-background',
|
||||||
|
sidebar: 'flex flex-col h-screen pt-2 bg-background',
|
||||||
|
logoHeader:
|
||||||
|
'flex w-full h-[56px] pl-[32px] pr-[24px] py-[8px] items-center justify-between shrink-0',
|
||||||
|
main: 'flex flex-1 flex-col h-screen overflow-hidden pt-2 pr-2 pb-2 pl-0',
|
||||||
|
// 헤더 아래 콘텐츠 면 (다크 헤더 rounded-t 와 합쳐져 카드 형태)
|
||||||
|
content: 'flex flex-1 flex-col min-h-0 w-full overflow-hidden rounded-b-[8px] bg-muted',
|
||||||
|
} 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 panes = (
|
||||||
|
<>
|
||||||
|
<aside className={cn(styles.sidebar, SIDEBAR_WIDTH[sidebarWidth])}>
|
||||||
|
<div className={styles.logoHeader}>
|
||||||
|
<Logo size="sm" />
|
||||||
|
{logoAction}
|
||||||
|
</div>
|
||||||
|
{sidebar}
|
||||||
|
</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
|
||||||
4
front/src/layouts/index.ts
Normal file
4
front/src/layouts/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export { MainLayout } from '@/layouts/MainLayout'
|
||||||
|
export type { MainLayoutProps, SidebarWidth } from '@/layouts/MainLayout'
|
||||||
|
export { MainHeaderBar } from '@/layouts/MainHeaderBar'
|
||||||
|
export type { MainHeaderBarProps } from '@/layouts/MainHeaderBar'
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { Provider } from '@/core/provider'
|
import { Provider } from '@/core/provider'
|
||||||
import './index.css'
|
import '@/index.css'
|
||||||
import App from './App.tsx'
|
import App from '@/App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
|||||||
@ -1,5 +1,47 @@
|
|||||||
|
import { useNavigate } from 'react-router'
|
||||||
|
import { List } from 'lucide-react'
|
||||||
|
import { MainLayout, MainHeaderBar } from '@/layouts'
|
||||||
|
|
||||||
export function ChatPage() {
|
export function ChatPage() {
|
||||||
return <main className="min-h-svh" />
|
return (
|
||||||
|
<MainLayout
|
||||||
|
sidebarWidth="chat"
|
||||||
|
scrollX
|
||||||
|
logoAction={<ListNavButton />}
|
||||||
|
sidebar={<Sidebar />}
|
||||||
|
header={
|
||||||
|
<MainHeaderBar className="px-[140px]">
|
||||||
|
<span className="text-xl font-semibold">협상 잔여 시간 : -</span>
|
||||||
|
</MainHeaderBar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* TODO: MainContent (ChatSection / MenuSection) */}
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ListNavButton() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="목록으로 이동"
|
||||||
|
onClick={() => navigate('/list')}
|
||||||
|
className="cursor-pointer text-foreground"
|
||||||
|
>
|
||||||
|
<List size={24} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* TODO: ItemSection */}
|
||||||
|
<div className="flex-1 overflow-y-auto" />
|
||||||
|
{/* TODO: FooterSection */}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChatPage
|
export default ChatPage
|
||||||
|
|||||||
@ -1,5 +1,30 @@
|
|||||||
|
import { MainLayout, MainHeaderBar } from '@/layouts'
|
||||||
|
import { FilterSection, SidebarFooter } from '@/features/list'
|
||||||
|
|
||||||
export function ListPage() {
|
export function ListPage() {
|
||||||
return <main className="min-h-svh" />
|
return (
|
||||||
|
<MainLayout
|
||||||
|
sidebarWidth="list"
|
||||||
|
sidebar={<Sidebar />}
|
||||||
|
header={
|
||||||
|
<MainHeaderBar align="center" className="px-[81px]">
|
||||||
|
<span className="text-xl font-bold whitespace-nowrap">견적 선택</span>
|
||||||
|
</MainHeaderBar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* TODO: Content (ButtonSection / TableSection / PageSection) */}
|
||||||
|
</MainLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Sidebar() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FilterSection />
|
||||||
|
{/* TODO: ArchiveSection */}
|
||||||
|
<SidebarFooter />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ListPage
|
export default ListPage
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user