feat(web): manage dictionaries and teams from the console

The console could not create dictionary entries, and team pages showed a
static member list with no record of who changed what. Dictionary
management, a knowledge upload form, and a team activity feed are now
available, alongside a download center that links the published desktop
installer feed rather than repository-local paths that no deploy ships.

Red-team e2e coverage was added for the account and team flows touched here.
This commit is contained in:
Yun Chan 2026-09-16 23:24:27 +09:00
parent f6a29db95a
commit cfc58458a8
21 changed files with 985 additions and 125 deletions

View file

@ -6,6 +6,7 @@ import { Box, Stack } from '@mui/material'
import { MetalCard, PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette, typoSx } from '@d3ro/ui/theme'
import { InviteMemberForm } from '@/components/teams/invite-member-form'
import { TeamActivityFeed, type TeamActivityItem } from '@/components/teams/activity-feed'
import { getSupabaseServerClient } from '@/lib/supabase-server'
interface PageProps {
@ -20,7 +21,7 @@ export default async function TeamDetailPage({ params }: PageProps): Promise<Rea
data: { user }
} = await supabase.auth.getUser()
const [{ data: team }, { data: members }, { data: meetings }] = await Promise.all([
const [{ data: team }, { data: members }, { data: meetings }, { data: activities }] = await Promise.all([
supabase.from('teams').select('id, name, owner_id, created_at').eq('id', id).maybeSingle(),
supabase
.from('team_members')
@ -31,7 +32,13 @@ export default async function TeamDetailPage({ params }: PageProps): Promise<Rea
.select('id, title, started_at, status')
.eq('team_id', id)
.order('started_at', { ascending: false })
.limit(20)
.limit(20),
supabase
.from('team_activities')
.select('id, actor_id, kind, body, created_at')
.eq('team_id', id)
.order('created_at', { ascending: false })
.limit(50)
])
if (!team) {
@ -41,6 +48,18 @@ export default async function TeamDetailPage({ params }: PageProps): Promise<Rea
const teamData = team as { id: string; name: string; owner_id: string; created_at: string }
const isOwner = teamData.owner_id === user?.id
const memberRows = (members ?? []) as unknown as Array<{
user_id: string
role: string
joined_at: string
profiles: { id: string; name: string | null; avatar_url: string | null } | null
}>
const isMember = memberRows.some((member) => member.user_id === user?.id)
const memberNames: Record<string, string> = {}
for (const member of memberRows) {
memberNames[member.user_id] = member.profiles?.name ?? member.user_id.slice(0, 8)
}
return (
<Box sx={{ p: 4 }}>
<Box sx={{ mb: 4 }}>
@ -138,6 +157,19 @@ export default async function TeamDetailPage({ params }: PageProps): Promise<Rea
</Stack>
)}
</MetalCard>
{/* 활동 피드 */}
<MetalCard sx={{ p: 3 }}>
<PhosphorText variant="heading" sx={{ mb: 2 }}>
ACTIVITY
</PhosphorText>
<TeamActivityFeed
teamId={teamData.id}
initialActivities={(activities ?? []) as TeamActivityItem[]}
memberNames={memberNames}
canPost={isMember}
/>
</MetalCard>
</Stack>
</Box>
)