Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { DevAuthSwitcher } from "@/components/DevAuthSwitcher";
import { DashboardLayout } from "@/components/DashboardLayout";
import "./globals.css";

export const metadata: Metadata = {
Expand All @@ -19,7 +20,7 @@ export default function RootLayout({
return (
<html lang="ko" className="h-full antialiased">
<body className="min-h-full flex flex-col">
{children}
<DashboardLayout>{children}</DashboardLayout>
<DevAuthSwitcher />
</body>
</html>
Expand Down
28 changes: 19 additions & 9 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useEffect, useState } from 'react';
import { DashboardLayout, type MobileWorkspaceView } from '@/components/DashboardLayout';

import { EmailList } from '@/components/EmailList';
import { EmailDetail } from '@/components/EmailDetail';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
Expand All @@ -16,12 +16,12 @@ function getDesktopWorkspaceMatch() {

export default function Home() {
const [selectedEmail, setSelectedEmail] = useState<number | null>(null);
const [mobileView, setMobileView] = useState<MobileWorkspaceView>('inbox');

const [isDesktopWorkspace, setIsDesktopWorkspace] = useState(true);
const showMobileActions = mobileView === 'actions' || mobileView === 'calendar';
const showMobileActions = false; // network graph hidden on mobile inbox view for simplicity
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
const handleSelectEmail = (emailId: number) => {
setSelectedEmail(emailId);
setMobileView('detail');

};

useEffect(() => {
Expand All @@ -34,7 +34,7 @@ export default function Home() {
}, []);

return (
<DashboardLayout mobileView={mobileView} onMobileViewChange={setMobileView}>
<>
{isDesktopWorkspace ? (
<ResizablePanelGroup orientation="horizontal" className="hidden h-full items-stretch rounded-3xl border border-border/80 bg-card/70 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur-xl lg:flex">
<ResizablePanel defaultSize={27} minSize={22}>
Expand Down Expand Up @@ -68,15 +68,25 @@ export default function Home() {
<div className="h-full overflow-hidden rounded-3xl border border-border/80 bg-card/70 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur-xl">
<section
aria-label="모바일 받은편지함"
className={`h-full ${mobileView === 'inbox' ? 'block' : 'hidden'}`}
className={`h-full ${!selectedEmail ? 'block' : 'hidden'}`}
>
<EmailList onSelectEmail={handleSelectEmail} selectedEmailId={selectedEmail} />
</section>
<section
aria-label="모바일 메일 상세"
className={`h-full ${mobileView === 'detail' ? 'block' : 'hidden'}`}
className={`h-full flex flex-col ${selectedEmail ? 'flex' : 'hidden'}`}
Comment thread
seonghobae marked this conversation as resolved.
Resolved
>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
<EmailDetail emailId={selectedEmail} />
<div className="p-3 border-b border-border bg-card">
<button
onClick={() => setSelectedEmail(null)}
className="text-sm font-semibold text-primary flex items-center gap-1"
>
← 목록으로
</button>
</div>
<div className="flex-1 min-h-0 overflow-hidden">
<EmailDetail emailId={selectedEmail} />
</div>
</section>
<section
aria-label="모바일 AI 실행"
Expand All @@ -99,6 +109,6 @@ export default function Home() {
</section>
</div>
)}
</DashboardLayout>
</>
);
}
37 changes: 4 additions & 33 deletions frontend/src/components/DashboardLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* @vitest-environment jsdom */
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";

Check warning on line 4 in frontend/src/components/DashboardLayout.test.tsx

View workflow job for this annotation

GitHub Actions / frontend

'vi' is defined but never used

import { DashboardLayout } from "./DashboardLayout";

Expand Down Expand Up @@ -36,7 +36,7 @@
const nav = container.querySelector('nav[aria-label="Naruon workspace sections"]');
const mobileNav = container.querySelector('nav[aria-label="Mobile workspace sections"]');
const mobileMenuButton = container.querySelector<HTMLButtonElement>('button[aria-label="Open workspace menu"]');
const mobileNavButtons = Array.from(mobileNav?.querySelectorAll('button') ?? []).map(
const mobileNavButtons = Array.from(mobileNav?.querySelectorAll('a') ?? []).map(
(button) => button.textContent,
);
const main = container.querySelector("main#main-content");
Expand All @@ -51,7 +51,8 @@
expect(mobileNav).not.toBeNull();
expect(mobileMenuButton?.getAttribute("aria-expanded")).toBe("false");
expect(mobileMenuButton?.getAttribute("aria-controls")).toBe("mobile-workspace-menu");
expect(mobileNavButtons).toEqual(["받은편지함", "맥락 검색", "AI 실행", "일정"]);
console.log('MOBILENAV HTML:', mobileNav?.outerHTML);
expect(mobileNavButtons).toEqual(["받은 메일", "AI Hub", "Prompt Studio", "워크스페이스 설정"]);
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
expect(main).not.toBeNull();
expect(skipLink).not.toBeNull();
expect(logo?.getAttribute("src")).toBe("/brand/naruon-logo.svg");
Expand All @@ -72,37 +73,7 @@
});

expect(mobileMenuButton?.getAttribute("aria-expanded")).toBe("true");
expect(container.querySelector('#mobile-workspace-menu')?.textContent ?? "").toContain("판단 포인트");
expect(container.querySelector('#mobile-workspace-menu')?.textContent ?? "").toContain("Prompt Studio");
Comment thread
seonghobae marked this conversation as resolved.
Resolved
});

it("routes mobile workspace menu items to primary mobile views", () => {
const handleMobileViewChange = vi.fn();
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);

act(() => {
root?.render(
<DashboardLayout mobileView="inbox" onMobileViewChange={handleMobileViewChange}>
<section>Inbox workspace content</section>
</DashboardLayout>,
);
});

const mobileMenuButton = container.querySelector<HTMLButtonElement>('button[aria-label="Open workspace menu"]');
act(() => {
mobileMenuButton?.click();
});

const executionLink = Array.from(
container.querySelectorAll<HTMLAnchorElement>('#mobile-workspace-menu a'),
).find((link) => link.textContent?.includes("실행 항목"));

act(() => {
executionLink?.click();
});

expect(handleMobileViewChange).toHaveBeenCalledWith("actions");
expect(mobileMenuButton?.getAttribute("aria-expanded")).toBe("false");
});
});
Comment thread
seonghobae marked this conversation as resolved.
Resolved
53 changes: 18 additions & 35 deletions frontend/src/components/DashboardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@
import {
CalendarDays,
CheckCircle2,
FileText,

Check warning on line 10 in frontend/src/components/DashboardLayout.tsx

View workflow job for this annotation

GitHub Actions / frontend

'FileText' is defined but never used
HelpCircle,
Home,

Check warning on line 12 in frontend/src/components/DashboardLayout.tsx

View workflow job for this annotation

GitHub Actions / frontend

'Home' is defined but never used
Inbox,
Menu,
Network,
PenLine,
Search,
Send,

Check warning on line 18 in frontend/src/components/DashboardLayout.tsx

View workflow job for this annotation

GitHub Actions / frontend

'Send' is defined but never used
Sparkles,
Settings,
Code,
Star,

Check warning on line 22 in frontend/src/components/DashboardLayout.tsx

View workflow job for this annotation

GitHub Actions / frontend

'Star' is defined but never used
Target,
} from 'lucide-react';

Expand All @@ -30,8 +30,6 @@
{ label: '워크스페이스 설정', description: 'LLM 및 보안 설정', icon: Settings, href: '/settings' },
];

export type MobileWorkspaceView = 'inbox' | 'detail' | 'actions' | 'calendar';

const aiNavItems = [
{ label: '받은편지함', description: '메일 스레드', icon: Inbox, active: true, mobileView: 'inbox' as const },
{ label: '맥락 종합', description: '흩어진 흐름 연결', icon: Network, mobileView: 'detail' as const },
Expand All @@ -46,12 +44,7 @@
{ label: '할 일 만들기', icon: CheckCircle2 },
];

const mobileNavItems = [
{ label: '받은편지함', icon: Inbox, view: 'inbox' as const },
{ label: '맥락 검색', icon: Search, view: 'detail' as const },
{ label: 'AI 실행', icon: Sparkles, view: 'actions' as const },
{ label: '일정', icon: CalendarDays, view: 'calendar' as const },
];


function NavLink({
label,
Expand Down Expand Up @@ -105,21 +98,11 @@

export function DashboardLayout({
children,
mobileView,
onMobileViewChange,
}: {
children: React.ReactNode;
mobileView?: MobileWorkspaceView;
onMobileViewChange?: (view: MobileWorkspaceView) => void;
}) {
const pathname = usePathname();
const [isWorkspaceMenuOpen, setIsWorkspaceMenuOpen] = useState(false);
const [uncontrolledMobileView, setUncontrolledMobileView] = useState<MobileWorkspaceView>('inbox');
const activeMobileView = mobileView ?? uncontrolledMobileView;
const setActiveMobileView = (view: MobileWorkspaceView) => {
if (!onMobileViewChange) setUncontrolledMobileView(view);
onMobileViewChange?.(view);
};

return (
<div className="relative flex h-screen overflow-hidden bg-background text-foreground">
Expand Down Expand Up @@ -225,42 +208,42 @@
<span className="rounded-full bg-primary/10 px-2.5 py-1 text-[11px] font-semibold text-primary">모바일</span>
</div>
<nav aria-label="Mobile workspace menu" className="grid gap-2">
{aiNavItems.map(({ label, description, icon: Icon, active, mobileView: itemMobileView }) => (
<a
{mailNavItems.map(({ label, description, icon: Icon, href }) => {
const active = pathname === href;
return (
<Link
key={label}
href="#main-content"
href={href}
aria-current={active ? 'page' : undefined}
onClick={() => {
setActiveMobileView(itemMobileView);
setIsWorkspaceMenuOpen(false);
}}
onClick={() => setIsWorkspaceMenuOpen(false)}
className="flex min-h-11 items-center gap-3 rounded-2xl border border-border/70 bg-background/70 px-3 py-2 text-sm font-semibold text-foreground focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/40"
>
<Icon className="size-4 text-primary" aria-hidden="true" />
<span className="flex flex-col leading-tight">
<span>{label}</span>
<span className="text-[11px] font-medium text-muted-foreground">{description}</span>
</span>
</a>
))}
</Link>
)})}
</nav>
</div>

<nav aria-label="Mobile workspace sections" className="fixed inset-x-3 bottom-3 z-40 grid grid-cols-4 rounded-3xl border border-border bg-card/95 p-2 shadow-[0_18px_50px_rgba(15,23,42,0.14)] backdrop-blur-xl lg:hidden">
{mobileNavItems.map(({ label, icon: Icon, view }) => {
const active = activeMobileView === view;
{mailNavItems.map(({ label, icon: Icon, href }) => {
const active = pathname === href;
return (
<button
<Link
key={label}
type="button"
onClick={() => setActiveMobileView(view)}
href={href}
data-mobile-view={label}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
aria-current={active ? 'page' : undefined}
data-mobile-view={view}
className={`flex min-h-11 flex-col items-center justify-center gap-1 rounded-2xl text-[11px] font-semibold ${active ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'}`}
className={`flex min-h-11 flex-col items-center justify-center gap-1 rounded-2xl text-[11px] font-semibold text-center ${
active ? 'bg-primary text-primary-foreground' : 'text-muted-foreground'
}`}
>
<Icon className="size-4" aria-hidden="true" />
{label}
</button>
</Link>
);
})}
</nav>
Expand Down
Loading