fix: SPRINT-017 review fixes
1. Desktop breakpoint 768px → 1280px+ (tablet keeps mobile flow) 2. Default context always rendered (shows working sister when none selected) 3. MobileSisterCard aria-pressed for accessibility 4. ChatWorkspace localStorage → cookie-based withSessionRequest
This commit is contained in:
@@ -276,7 +276,7 @@ const PageMeta = styled.div`
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@@ -287,7 +287,7 @@ const FreshnessBar = styled.div`
|
||||
flex-wrap: wrap;
|
||||
margin-left: auto;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -304,14 +304,14 @@ const FreshnessBadge = styled.span<{ $type: 'live' | 'snapshot' | 'fallback' }>`
|
||||
opacity: 0.8;
|
||||
`;
|
||||
|
||||
/* ─── Desktop layout (≥768px): scene + context side-by-side ─── */
|
||||
/* ─── Desktop layout (≥1280px): scene + context side-by-side ─── */
|
||||
const DesktopMain = styled.div`
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@@ -328,7 +328,7 @@ const ChatArea = styled.div`
|
||||
height: 520px;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
height: 400px;
|
||||
}
|
||||
`;
|
||||
@@ -338,7 +338,7 @@ const BottomPanels = styled.div`
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
@@ -355,13 +355,13 @@ const LoadingOverlay = styled.div`
|
||||
letter-spacing: 0.08em;
|
||||
`;
|
||||
|
||||
/* ─── Mobile layout (<768px) ─── */
|
||||
/* ─── Mobile layout (<1280px) ─── */
|
||||
const MobileFlow = styled.div`
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: var(--space-lg);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
@media (max-width: 1279px) {
|
||||
display: flex;
|
||||
}
|
||||
`;
|
||||
@@ -707,7 +707,7 @@ export default function OfficePage() {
|
||||
</FreshnessBar>
|
||||
</PageHeader>
|
||||
|
||||
{/* ─── Desktop layout (≥768px) ─── */}
|
||||
{/* ─── Desktop layout (≥1280px) ─── */}
|
||||
<DesktopMain>
|
||||
<SceneColumn>
|
||||
<OfficeScene
|
||||
@@ -734,7 +734,7 @@ export default function OfficePage() {
|
||||
/>
|
||||
</DesktopMain>
|
||||
|
||||
{/* ─── Mobile layout (<768px) ─── */}
|
||||
{/* ─── Mobile layout (<1280px) ─── */}
|
||||
<MobileFlow>
|
||||
{/* 1. Sister status cards */}
|
||||
<MobileSisterGrid>
|
||||
@@ -745,6 +745,7 @@ export default function OfficePage() {
|
||||
key={sister.name}
|
||||
$state={sister.state}
|
||||
$selected={isSelected}
|
||||
aria-pressed={isSelected}
|
||||
onClick={() => handleSelectSister(sister.name)}
|
||||
>
|
||||
<SisterAvatar name={sister.name} size={32} />
|
||||
@@ -785,34 +786,38 @@ export default function OfficePage() {
|
||||
))}
|
||||
</MobileQuickActions>
|
||||
|
||||
{/* 4. Inline context (selected sister detail) */}
|
||||
{selected && selectedSisterNode && (
|
||||
<MobileContextInline>
|
||||
<MobileContextHeader>
|
||||
<MobileContextTitle>
|
||||
{selected.type === 'sister' ? selectedSisterNode.displayName : selectedSubagent?.label ?? ''} · context
|
||||
</MobileContextTitle>
|
||||
<MobileCloseBtn onClick={() => setSelected(null)}>✕</MobileCloseBtn>
|
||||
</MobileContextHeader>
|
||||
<MobileSelectedSummary>
|
||||
<SisterAvatar name={selectedSisterNode.name} size={28} />
|
||||
<MobileSelectedMeta>
|
||||
<MobileSelectedName>
|
||||
{selected.type === 'sister' ? selectedSisterNode.displayName : selectedSubagent?.label}
|
||||
</MobileSelectedName>
|
||||
<MobileSelectedRole>{selectedSisterNode.role}</MobileSelectedRole>
|
||||
</MobileSelectedMeta>
|
||||
</MobileSelectedSummary>
|
||||
{(selectedSisterNode.currentTask || selectedSisterNode.activeSessionLabel) && (
|
||||
<MobileTaskBox>
|
||||
{selectedSisterNode.currentTask ?? selectedSisterNode.activeSessionLabel}
|
||||
</MobileTaskBox>
|
||||
)}
|
||||
<MobileQuickActions>
|
||||
<MobileActionBtn onClick={() => handleOpenChat(selectedSisterNode.name)}>채팅</MobileActionBtn>
|
||||
</MobileQuickActions>
|
||||
</MobileContextInline>
|
||||
)}
|
||||
{/* 4. Inline context — always show default, expand on selection */}
|
||||
<MobileContextInline>
|
||||
<MobileContextHeader>
|
||||
<MobileContextTitle>
|
||||
{selected && selectedSisterNode
|
||||
? `${selected.type === 'sister' ? selectedSisterNode.displayName : selectedSubagent?.label ?? ''} · context`
|
||||
: '현재 포커스'}
|
||||
</MobileContextTitle>
|
||||
{selected && <MobileCloseBtn onClick={() => setSelected(null)}>✕</MobileCloseBtn>}
|
||||
</MobileContextHeader>
|
||||
<MobileSelectedSummary>
|
||||
<SisterAvatar name={(selectedSisterNode ?? sisterNodes[0])?.name ?? 'harang'} size={28} />
|
||||
<MobileSelectedMeta>
|
||||
<MobileSelectedName>
|
||||
{selected && selectedSisterNode
|
||||
? (selected.type === 'sister' ? selectedSisterNode.displayName : selectedSubagent?.label)
|
||||
: sisterNodes.find((s) => s.state === 'tool_calling' || s.state === 'thinking')?.displayName ?? sisterNodes[0]?.displayName ?? '—'}
|
||||
</MobileSelectedName>
|
||||
<MobileSelectedRole>
|
||||
{selectedSisterNode?.role ?? sisterNodes[0]?.role ?? ''}
|
||||
</MobileSelectedRole>
|
||||
</MobileSelectedMeta>
|
||||
</MobileSelectedSummary>
|
||||
{((selectedSisterNode?.currentTask ?? sisterNodes.find((s) => s.state === 'tool_calling' || s.state === 'thinking')?.currentTask)) && (
|
||||
<MobileTaskBox>
|
||||
{selectedSisterNode?.currentTask ?? sisterNodes.find((s) => s.state === 'tool_calling' || s.state === 'thinking')?.currentTask ?? ''}
|
||||
</MobileTaskBox>
|
||||
)}
|
||||
<MobileQuickActions>
|
||||
<MobileActionBtn onClick={() => handleOpenChat((selectedSisterNode ?? sisterNodes[0])?.name ?? 'harang')}>채팅</MobileActionBtn>
|
||||
</MobileQuickActions>
|
||||
</MobileContextInline>
|
||||
|
||||
{/* 5. Chat (if open) */}
|
||||
{chatSister && (
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
import SisterAvatar from '@/components/common/SisterAvatar';
|
||||
import { API_URL } from '@/lib/config';
|
||||
import { withSessionRequest } from '@/lib/csrf';
|
||||
import type { SisterName, AgentState } from './OfficeScene';
|
||||
|
||||
interface RuntimeMessage {
|
||||
@@ -409,10 +410,8 @@ function runtimeStateLabel(state: AgentState): string {
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
function getToken(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return localStorage.getItem('hanarang_access_token') ?? '';
|
||||
}
|
||||
// Fix #4: Remove localStorage token — use cookie-based auth via withSessionRequest
|
||||
// Token is handled by HttpOnly cookies + CSRF, no client-side access needed
|
||||
|
||||
function mergeMessages(prev: ChatMessage[], incoming: ChatMessage[]) {
|
||||
const map = new Map<string, ChatMessage>();
|
||||
@@ -443,7 +442,7 @@ export default function ChatWorkspace({ initialSister, onClose }: ChatWorkspaceP
|
||||
|
||||
const messages = useMemo(() => allMessages[activeSister] ?? [], [allMessages, activeSister]);
|
||||
const runtime = runtimeBySister[activeSister] ?? null;
|
||||
const hasToken = Boolean(getToken());
|
||||
// Cookie-based auth always available (no token check needed)
|
||||
|
||||
useEffect(() => {
|
||||
setActiveSister(initialSister);
|
||||
@@ -489,16 +488,7 @@ export default function ChatWorkspace({ initialSister, onClose }: ChatWorkspaceP
|
||||
const text = input.trim();
|
||||
if (!text || sending) return;
|
||||
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
setMessages(activeSister, (prev) => mergeMessages(prev, [{
|
||||
id: `sys-${crypto.randomUUID()}`,
|
||||
role: 'assistant',
|
||||
content: '로그인이 풀린 것 같아. 다시 로그인한 뒤 시도해줘.',
|
||||
ts: new Date().toISOString(),
|
||||
}]));
|
||||
return;
|
||||
}
|
||||
// Cookie-based auth — no token check needed
|
||||
|
||||
const userMessage: ChatMessage = {
|
||||
id: `user-${crypto.randomUUID()}`,
|
||||
@@ -512,14 +502,11 @@ export default function ChatWorkspace({ initialSister, onClose }: ChatWorkspaceP
|
||||
setSending(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_URL}/api/sisters/${activeSister}/chat`, {
|
||||
const res = await fetch(`${API_URL}/api/sisters/${activeSister}/chat`, withSessionRequest({
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message: text }),
|
||||
});
|
||||
}, { csrf: true }));
|
||||
|
||||
const data = await res.json();
|
||||
if (!res.ok) {
|
||||
@@ -622,7 +609,7 @@ export default function ChatWorkspace({ initialSister, onClose }: ChatWorkspaceP
|
||||
onKeyDown={handleKeyDown}
|
||||
rows={1}
|
||||
/>
|
||||
<SendBtn onClick={() => void handleSend()} disabled={!input.trim() || sending || !hasToken}>
|
||||
<SendBtn onClick={() => void handleSend()} disabled={!input.trim() || sending}>
|
||||
{sending ? '전송 중' : '전송'}
|
||||
</SendBtn>
|
||||
</InputArea>
|
||||
|
||||
Reference in New Issue
Block a user