feat: add LMS web client

This commit is contained in:
2026-07-15 13:41:00 +09:00
commit 05ff2644e8
308 changed files with 487923 additions and 0 deletions
@@ -0,0 +1,128 @@
import { CarFront, Laptop, Monitor } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import type React from 'react'
import { PlatformOptionCard, RunningPlatformCard } from './PlatformCards'
import PlatformFilterTabs, { type PlatformFilter } from './PlatformFilterTabs'
import type { PlatformOption } from './types'
const PLATFORM_ICONS: Record<string, LucideIcon> = {
laptop: Laptop,
monitor: Monitor,
car: CarFront
}
type PlatformSelectionPanelProps = {
mode: 'select' | 'settings'
runningPlatforms: PlatformOption[]
filteredPlatforms: PlatformOption[]
filter: PlatformFilter
counts: Record<PlatformFilter, number>
workspaceWeekTitle: string
platformLoading: boolean
isPlatformUnavailable: (platform: PlatformOption) => boolean
getPlatformDotClass: (platform: PlatformOption) => string
onFilterChange: (filter: PlatformFilter) => void
onRunningOpen: (platform: PlatformOption) => void
onRunningClose: (platform: PlatformOption) => void
onPlatformAction: (platform: PlatformOption) => void
onOpenSettings: (platform: PlatformOption) => void
}
export default function PlatformSelectionPanel({
mode,
runningPlatforms,
filteredPlatforms,
filter,
counts,
workspaceWeekTitle,
platformLoading,
isPlatformUnavailable,
getPlatformDotClass,
onFilterChange,
onRunningOpen,
onRunningClose,
onPlatformAction,
onOpenSettings
}: PlatformSelectionPanelProps): React.JSX.Element {
return (
<div className={['min-h-0 w-1/2 shrink-0 px-6 pb-5 pt-3 transition-opacity duration-300', mode === 'settings' ? 'opacity-0' : 'opacity-100'].join(' ')}>
{runningPlatforms.length > 0 && (
<div className="mb-4 pr-1">
<p className="mb-2 text-[12px] font-black text-[#2563eb]">실행중인 플랫폼</p>
<div className="grid gap-3">
{runningPlatforms.map((platform) => {
const Icon = PLATFORM_ICONS[platform.iconKey] ?? Monitor
return (
<RunningPlatformCard
key={platform.id}
platform={platform}
Icon={Icon}
dotClassName={getPlatformDotClass(platform)}
weekTitle={workspaceWeekTitle}
disabled={platformLoading}
onOpen={() => onRunningOpen(platform)}
onClose={() => onRunningClose(platform)}
/>
)
})}
</div>
</div>
)}
{runningPlatforms.length > 0 && (
<div className="mb-3 h-px bg-[#e5e8ef]" />
)}
<PlatformFilterTabs
value={filter}
counts={counts}
onChange={onFilterChange}
/>
<div data-help="platform-list" className={[
'grid content-start gap-3 overflow-y-auto pr-1',
runningPlatforms.length > 0
? 'min-h-[340px] max-h-[340px]'
: 'min-h-[470px] max-h-[470px]'
].join(' ')}>
{filteredPlatforms.length ? filteredPlatforms.map((platform) => {
const { id, status, action, state, iconKey } = platform
const Icon = PLATFORM_ICONS[iconKey] ?? Monitor
const locked = state === 'locked'
const unavailable = isPlatformUnavailable(platform)
const actionDisabled = locked || unavailable
const runningInSelectionList = runningPlatforms.some((runningPlatform) => runningPlatform.id === platform.id)
const displayStatus = unavailable && !status.includes('연결 끊김') && !status.includes('연결 불가')
? '연결 끊김'
: status
const hasControl = platform.control?.status === 'ACTIVE'
const primaryAction = hasControl ? '연결' : action
const platformActionDisabled = runningInSelectionList || actionDisabled
return (
<PlatformOptionCard
key={id}
platform={platform}
Icon={Icon}
dotClassName={getPlatformDotClass(platform)}
unavailable={unavailable}
displayStatus={displayStatus}
primaryAction={primaryAction}
actionDisabled={platformActionDisabled}
running={runningInSelectionList}
onAction={() => {
if (runningInSelectionList) return
onPlatformAction(platform)
}}
onSettings={() => onOpenSettings(platform)}
/>
)
}) : (
<div className="grid min-h-[220px] place-items-center rounded-lg border border-dashed border-[#d9deea] bg-[#fbfcff] px-6 text-center">
<div>
<strong className="block text-[15px] font-bold text-[#111520]">사용 가능한 플랫폼 데이터가 없습니다.</strong>
<p className="mt-2 text-[13px] font-medium text-[#687284]">강의에 배정된 플랫폼을 불러오지 못했거나 아직 등록된 데이터가 없습니다.</p>
</div>
</div>
)}
</div>
</div>
)
}