platform/lms-adm-fe release promotion snapshot
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hash": "c60753b8",
|
||||||
|
"configHash": "84cc1fb0",
|
||||||
|
"lockfileHash": "707b26ba",
|
||||||
|
"browserHash": "992b5a32",
|
||||||
|
"optimized": {},
|
||||||
|
"chunks": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type": "module"
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { CheckCircle2, CirclePlay, Clock3, FileCode2, FileText, Loader2, Map, Plus, RadioTower, Save, Target, Trash2, Upload, type LucideIcon } from 'lucide-react';
|
import { CheckCircle2, CirclePlay, Clock3, FileCode2, FileText, Loader2, Map, Plus, RadioTower, Save, Target, Trash2, Upload, type LucideIcon } from 'lucide-react';
|
||||||
import { ConfirmDialog } from '../../components/ConfirmDialog';
|
import { ConfirmDialog } from '../../components/ConfirmDialog';
|
||||||
import { PageHeader } from '../../components/PageHeader';
|
import { PageHeader } from '../../components/PageHeader';
|
||||||
@@ -14,6 +15,21 @@ type ModuleParameterDraft = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EditableEnvironmentProfile = CurriculumEnvironmentProfile & {
|
||||||
|
storageKey: 'local' | 'platform';
|
||||||
|
platformKeys: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function isPlatformEnvironment(config: unknown): boolean {
|
||||||
|
if (!config || typeof config !== 'object' || Array.isArray(config)) return false;
|
||||||
|
const target = (config as Record<string, unknown>).target;
|
||||||
|
return target === 'remote-jetson' || target === 'platform';
|
||||||
|
}
|
||||||
|
|
||||||
|
function environmentStorageKey(profile: Pick<CurriculumEnvironmentProfile, 'config'>): 'local' | 'platform' {
|
||||||
|
return isPlatformEnvironment(profile.config) ? 'platform' : 'local';
|
||||||
|
}
|
||||||
|
|
||||||
function parseModuleParameter(parameter: string): ModuleParameterDraft {
|
function parseModuleParameter(parameter: string): ModuleParameterDraft {
|
||||||
const separatorIndex = parameter.indexOf(':=');
|
const separatorIndex = parameter.indexOf(':=');
|
||||||
if (separatorIndex < 0) return { key: parameter.trim(), value: '' };
|
if (separatorIndex < 0) return { key: parameter.trim(), value: '' };
|
||||||
@@ -38,37 +54,56 @@ function readEnvironmentModuleIds(profile: CurriculumEnvironmentProfile, modules
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
function readEnvironmentModuleParameters(profile: CurriculumEnvironmentProfile): Record<string, ModuleParameterDraft[]> {
|
function readEnvironmentModuleParameters(
|
||||||
|
profile: CurriculumEnvironmentProfile,
|
||||||
|
modules: NonNullable<CurriculumWeek['modules']>,
|
||||||
|
): Record<string, ModuleParameterDraft[]> {
|
||||||
const config = profile.config;
|
const config = profile.config;
|
||||||
const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet)
|
const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet)
|
||||||
? config.moduleSet as Record<string, unknown>
|
? config.moduleSet as Record<string, unknown>
|
||||||
: null;
|
: null;
|
||||||
if (!moduleSet || !Array.isArray(moduleSet.modules)) return {};
|
const configuredModules = new globalThis.Map<string, Record<string, unknown>>(
|
||||||
|
!moduleSet || !Array.isArray(moduleSet.modules)
|
||||||
|
? []
|
||||||
|
: moduleSet.modules.flatMap((item) => (
|
||||||
|
item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'
|
||||||
|
? [[(item as Record<string, unknown>).id as string, item as Record<string, unknown>]]
|
||||||
|
: []
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
const moduleDefaults = new globalThis.Map(modules.map((module) => [module.id, module.defaultParameters ?? []]));
|
||||||
|
|
||||||
return Object.fromEntries(moduleSet.modules.flatMap((item) => {
|
return Object.fromEntries(readEnvironmentModuleIds(profile, modules).map((moduleId) => {
|
||||||
if (!item || typeof item !== 'object') return [];
|
const configured = configuredModules.get(moduleId);
|
||||||
const module = item as Record<string, unknown>;
|
const parameters = Array.isArray(configured?.defaultParameters)
|
||||||
if (typeof module.id !== 'string') return [];
|
? configured.defaultParameters.filter((value): value is string => typeof value === 'string')
|
||||||
const parameters = Array.isArray(module.defaultParameters)
|
: moduleDefaults.get(moduleId) ?? [];
|
||||||
? module.defaultParameters.filter((value): value is string => typeof value === 'string').map(parseModuleParameter)
|
return [moduleId, parameters.map(parseModuleParameter)];
|
||||||
: [];
|
|
||||||
return [[module.id, parameters]];
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function readEnvironmentModuleDockerImages(profile: CurriculumEnvironmentProfile): Record<string, string> {
|
function readEnvironmentModuleDockerImages(
|
||||||
|
profile: CurriculumEnvironmentProfile,
|
||||||
|
modules: NonNullable<CurriculumWeek['modules']>,
|
||||||
|
): Record<string, string> {
|
||||||
const config = profile.config;
|
const config = profile.config;
|
||||||
const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet)
|
const moduleSet = config && typeof config.moduleSet === 'object' && config.moduleSet !== null && !Array.isArray(config.moduleSet)
|
||||||
? config.moduleSet as Record<string, unknown>
|
? config.moduleSet as Record<string, unknown>
|
||||||
: null;
|
: null;
|
||||||
if (!moduleSet || !Array.isArray(moduleSet.modules)) return {};
|
const configuredModules = new globalThis.Map<string, Record<string, unknown>>(
|
||||||
|
!moduleSet || !Array.isArray(moduleSet.modules)
|
||||||
|
? []
|
||||||
|
: moduleSet.modules.flatMap((item) => (
|
||||||
|
item && typeof item === 'object' && typeof (item as Record<string, unknown>).id === 'string'
|
||||||
|
? [[(item as Record<string, unknown>).id as string, item as Record<string, unknown>]]
|
||||||
|
: []
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
const moduleDefaults = new globalThis.Map(modules.map((module) => [module.id, module.dockerImage ?? '']));
|
||||||
|
|
||||||
return Object.fromEntries(moduleSet.modules.flatMap((item) => {
|
return Object.fromEntries(readEnvironmentModuleIds(profile, modules).map((moduleId) => {
|
||||||
if (!item || typeof item !== 'object') return [];
|
const configuredImage = configuredModules.get(moduleId)?.dockerImage;
|
||||||
const module = item as Record<string, unknown>;
|
return [moduleId, typeof configuredImage === 'string' ? configuredImage : moduleDefaults.get(moduleId) ?? ''];
|
||||||
return typeof module.id === 'string' && typeof module.dockerImage === 'string'
|
|
||||||
? [[module.id, module.dockerImage]]
|
|
||||||
: [];
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +146,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
const [referenceFile, setReferenceFile] = useState<File | null>(null);
|
const [referenceFile, setReferenceFile] = useState<File | null>(null);
|
||||||
const [exampleCodeTitle, setExampleCodeTitle] = useState('');
|
const [exampleCodeTitle, setExampleCodeTitle] = useState('');
|
||||||
const [exampleCodeFile, setExampleCodeFile] = useState<File | null>(null);
|
const [exampleCodeFile, setExampleCodeFile] = useState<File | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<'apply' | 'edit' | 'modules' | 'examples'>('apply');
|
const [activeTab, setActiveTab] = useState<'apply' | 'edit' | 'modules' | 'maps' | 'examples'>('apply');
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
@@ -135,6 +170,42 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
() => `${CURRICULUM_SELECTED_GROUP_STORAGE_PREFIX}:${session.user.id}:${selectedCourseId || 'default'}`,
|
() => `${CURRICULUM_SELECTED_GROUP_STORAGE_PREFIX}:${session.user.id}:${selectedCourseId || 'default'}`,
|
||||||
[selectedCourseId, session.user.id],
|
[selectedCourseId, session.user.id],
|
||||||
);
|
);
|
||||||
|
const editableEnvironmentProfiles = useMemo<EditableEnvironmentProfile[]>(() => {
|
||||||
|
if (!selectedWeek) return [];
|
||||||
|
|
||||||
|
const savedProfiles = new globalThis.Map((selectedWeek.environmentProfiles ?? []).map((profile) => [profile.platformKey, profile]));
|
||||||
|
const profilesByEnvironment = new globalThis.Map<'local' | 'platform', EditableEnvironmentProfile>();
|
||||||
|
|
||||||
|
visualizationPlatforms
|
||||||
|
.filter((platform) => visualizationEnabledByKey[platform.key])
|
||||||
|
.forEach((platform) => {
|
||||||
|
const profile = savedProfiles.get(platform.key) ?? {
|
||||||
|
id: `draft:${selectedWeek.week}:${platform.key}`,
|
||||||
|
platformKey: platform.key,
|
||||||
|
profileType: platform.type,
|
||||||
|
name: platform.displayName,
|
||||||
|
config: platform.config && typeof platform.config === 'object' && !Array.isArray(platform.config)
|
||||||
|
? platform.config as Record<string, unknown>
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
const storageKey = environmentStorageKey(profile);
|
||||||
|
const existing = profilesByEnvironment.get(storageKey);
|
||||||
|
if (existing) {
|
||||||
|
existing.platformKeys.push(platform.key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
profilesByEnvironment.set(storageKey, {
|
||||||
|
...profile,
|
||||||
|
id: `draft:${selectedWeek.week}:${storageKey}`,
|
||||||
|
name: storageKey === 'local' ? '로컬' : '플랫폼',
|
||||||
|
profileType: storageKey === 'local' ? 'LOCAL' : 'PLATFORM',
|
||||||
|
storageKey,
|
||||||
|
platformKeys: [platform.key],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...profilesByEnvironment.values()];
|
||||||
|
}, [selectedWeek, visualizationEnabledByKey, visualizationPlatforms]);
|
||||||
|
|
||||||
const resolveProgressSelection = (nextWeeks: CurriculumWeek[], setting: InstructorCurriculumResponse['setting']) => {
|
const resolveProgressSelection = (nextWeeks: CurriculumWeek[], setting: InstructorCurriculumResponse['setting']) => {
|
||||||
const nextWeek = nextWeeks.find((week) => week.state === 'current')?.week
|
const nextWeek = nextWeeks.find((week) => week.state === 'current')?.week
|
||||||
@@ -239,17 +310,21 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
setDraftPracticeContents(selectedWeek.detail.practiceContents?.length ? selectedWeek.detail.practiceContents : []);
|
setDraftPracticeContents(selectedWeek.detail.practiceContents?.length ? selectedWeek.detail.practiceContents : []);
|
||||||
setModuleEnabledById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, module.enabled !== false])));
|
setModuleEnabledById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, module.enabled !== false])));
|
||||||
setModuleDefaultParametersById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, (module.defaultParameters ?? []).map(parseModuleParameter)])));
|
setModuleDefaultParametersById(Object.fromEntries((selectedWeek.modules ?? []).map((module) => [module.id, (module.defaultParameters ?? []).map(parseModuleParameter)])));
|
||||||
setEnvironmentModuleIdsByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [
|
const profilesByEnvironment = (selectedWeek.environmentProfiles ?? []).reduce<Record<'local' | 'platform', CurriculumEnvironmentProfile[]>>((groups, profile) => {
|
||||||
profile.platformKey,
|
groups[environmentStorageKey(profile)].push(profile);
|
||||||
readEnvironmentModuleIds(profile, selectedWeek.modules ?? []),
|
return groups;
|
||||||
|
}, { local: [], platform: [] });
|
||||||
|
setEnvironmentModuleIdsByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [
|
||||||
|
key,
|
||||||
|
[...new Set(profiles.flatMap((profile) => readEnvironmentModuleIds(profile, selectedWeek.modules ?? [])))],
|
||||||
])));
|
])));
|
||||||
setEnvironmentModuleParametersByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [
|
setEnvironmentModuleParametersByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [
|
||||||
profile.platformKey,
|
key,
|
||||||
readEnvironmentModuleParameters(profile),
|
Object.assign({}, ...profiles.map((profile) => readEnvironmentModuleParameters(profile, selectedWeek.modules ?? []))),
|
||||||
])));
|
])));
|
||||||
setEnvironmentModuleDockerImagesByPlatformKey(Object.fromEntries((selectedWeek.environmentProfiles ?? []).map((profile) => [
|
setEnvironmentModuleDockerImagesByPlatformKey(Object.fromEntries(Object.entries(profilesByEnvironment).map(([key, profiles]) => [
|
||||||
profile.platformKey,
|
key,
|
||||||
readEnvironmentModuleDockerImages(profile),
|
Object.assign({}, ...profiles.map((profile) => readEnvironmentModuleDockerImages(profile, selectedWeek.modules ?? []))),
|
||||||
])));
|
])));
|
||||||
setVisualizationEnabledByKey(Object.fromEntries((selectedWeek.visualizationPlatforms ?? []).map((platform) => [platform.key, true])));
|
setVisualizationEnabledByKey(Object.fromEntries((selectedWeek.visualizationPlatforms ?? []).map((platform) => [platform.key, true])));
|
||||||
setMapEnabledByKey(Object.fromEntries((selectedWeek.mapKeys ?? []).map((key) => [key, true])));
|
setMapEnabledByKey(Object.fromEntries((selectedWeek.mapKeys ?? []).map((key) => [key, true])));
|
||||||
@@ -424,17 +499,20 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
.filter((parameter) => parameter.key.trim())
|
.filter((parameter) => parameter.key.trim())
|
||||||
.map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`),
|
.map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`),
|
||||||
}));
|
}));
|
||||||
const environmentModuleSets = (selectedWeek.environmentProfiles ?? []).map((profile) => ({
|
const environmentModuleSets = editableEnvironmentProfiles.flatMap((profile) => {
|
||||||
platformKey: profile.platformKey,
|
const moduleIds = environmentModuleIdsByPlatformKey[profile.storageKey] ?? (selectedWeek.modules ?? []).map((module) => module.id);
|
||||||
moduleIds: environmentModuleIdsByPlatformKey[profile.platformKey] ?? [],
|
return profile.platformKeys.map((platformKey) => ({
|
||||||
modules: (environmentModuleIdsByPlatformKey[profile.platformKey] ?? []).map((id) => ({
|
platformKey,
|
||||||
|
moduleIds,
|
||||||
|
modules: moduleIds.map((id) => ({
|
||||||
id,
|
id,
|
||||||
defaultParameters: (environmentModuleParametersByPlatformKey[profile.platformKey]?.[id] ?? [])
|
defaultParameters: (environmentModuleParametersByPlatformKey[profile.storageKey]?.[id] ?? [])
|
||||||
.filter((parameter) => parameter.key.trim())
|
.filter((parameter) => parameter.key.trim())
|
||||||
.map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`),
|
.map((parameter) => `${parameter.key.trim()}:=${parameter.value.trim()}`),
|
||||||
dockerImage: environmentModuleDockerImagesByPlatformKey[profile.platformKey]?.[id]?.trim() || undefined,
|
dockerImage: environmentModuleDockerImagesByPlatformKey[profile.storageKey]?.[id]?.trim() || undefined,
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
});
|
||||||
const payload = await updateInstructorCurriculum(session.accessToken, {
|
const payload = await updateInstructorCurriculum(session.accessToken, {
|
||||||
courseId: selectedCourseId || null,
|
courseId: selectedCourseId || null,
|
||||||
groupId: selectedGroupId || null,
|
groupId: selectedGroupId || null,
|
||||||
@@ -444,9 +522,6 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
disabledModuleIds,
|
disabledModuleIds,
|
||||||
moduleDefaultParameters,
|
moduleDefaultParameters,
|
||||||
environmentModuleSets,
|
environmentModuleSets,
|
||||||
visualizationPlatformKeys: visualizationPlatforms
|
|
||||||
.filter((platform) => visualizationEnabledByKey[platform.key])
|
|
||||||
.map((platform) => platform.key),
|
|
||||||
mapKeys: mapOptions.filter((map) => mapEnabledByKey[map.key]).map((map) => map.key),
|
mapKeys: mapOptions.filter((map) => mapEnabledByKey[map.key]).map((map) => map.key),
|
||||||
});
|
});
|
||||||
applyPayload(payload);
|
applyPayload(payload);
|
||||||
@@ -714,6 +789,13 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
>
|
>
|
||||||
모듈 관리
|
모듈 관리
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-8 rounded-md px-4 text-[13px] font-black transition ${activeTab === 'maps' ? 'bg-[#2563eb] text-white shadow-[0_8px_18px_rgba(37,99,235,0.18)]' : 'text-[#475569] hover:bg-[#e2e8f0] hover:text-[#111827]'}`}
|
||||||
|
onClick={() => setActiveTab('maps')}
|
||||||
|
>
|
||||||
|
맵 설정
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`h-8 rounded-md px-4 text-[13px] font-black transition ${activeTab === 'examples' ? 'bg-[#2563eb] text-white shadow-[0_8px_18px_rgba(37,99,235,0.18)]' : 'text-[#475569] hover:bg-[#e2e8f0] hover:text-[#111827]'}`}
|
className={`h-8 rounded-md px-4 text-[13px] font-black transition ${activeTab === 'examples' ? 'bg-[#2563eb] text-white shadow-[0_8px_18px_rgba(37,99,235,0.18)]' : 'text-[#475569] hover:bg-[#e2e8f0] hover:text-[#111827]'}`}
|
||||||
@@ -737,9 +819,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{activeTab !== 'examples' && !(activeTab === 'apply' && selectedWeekCompleted) ? (
|
{activeTab !== 'examples' && !(activeTab === 'apply' && selectedWeekCompleted) ? (
|
||||||
<button className="inline-flex h-10 items-center gap-2 rounded-md bg-[#2563eb] px-4 text-[13px] font-bold text-white hover:bg-[#1d4ed8] disabled:cursor-not-allowed disabled:opacity-60" type="button" onClick={() => void (activeTab === 'apply' ? handleApply() : activeTab === 'modules' ? handleSaveModules() : handleSaveContent())} disabled={primaryActionDisabled}>
|
<button className="inline-flex h-10 items-center gap-2 rounded-md bg-[#2563eb] px-4 text-[13px] font-bold text-white hover:bg-[#1d4ed8] disabled:cursor-not-allowed disabled:opacity-60" type="button" onClick={() => void (activeTab === 'apply' ? handleApply() : activeTab === 'modules' || activeTab === 'maps' ? handleSaveModules() : handleSaveContent())} disabled={primaryActionDisabled}>
|
||||||
{saving ? <Loader2 className="animate-spin" size={15} aria-hidden="true" /> : activeTab === 'apply' ? <CirclePlay size={15} aria-hidden="true" /> : <Save size={15} aria-hidden="true" />}
|
{saving ? <Loader2 className="animate-spin" size={15} aria-hidden="true" /> : activeTab === 'apply' ? <CirclePlay size={15} aria-hidden="true" /> : <Save size={15} aria-hidden="true" />}
|
||||||
{activeTab === 'apply' ? '진행하기' : activeTab === 'modules' ? '모듈 저장' : '내용 저장'}
|
{activeTab === 'apply' ? '진행하기' : activeTab === 'modules' ? '모듈 저장' : activeTab === 'maps' ? '맵 저장' : '내용 저장'}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{activeTab === 'apply' ? (
|
{activeTab === 'apply' ? (
|
||||||
@@ -880,127 +962,24 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : activeTab === 'modules' ? (
|
) : activeTab === 'modules' ? (
|
||||||
<div className="grid gap-4 p-5">
|
<div className="grid gap-4 p-5">
|
||||||
<section>
|
{editableEnvironmentProfiles.length > 0 ? (
|
||||||
<div className="flex items-center justify-between gap-3">
|
<section>
|
||||||
<div>
|
|
||||||
<h3 className="text-[15px] font-black text-[#111827]">모듈 관리</h3>
|
|
||||||
<p className="mt-1 text-[12px] font-bold text-[#64748b]">체크된 모듈만 학생 앱의 모듈 리스트에 표시됩니다.</p>
|
|
||||||
</div>
|
|
||||||
{selectedWeek?.modules?.length ? (
|
|
||||||
<span className="rounded-md bg-[#eff6ff] px-3 py-1.5 text-[12px] font-black text-[#2563eb]">
|
|
||||||
{(selectedWeek.modules ?? []).filter((module) => moduleEnabledById[module.id] !== false).length}/{selectedWeek.modules.length}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 grid gap-2">
|
|
||||||
{selectedWeek?.modules?.length ? selectedWeek.modules.map((module) => {
|
|
||||||
const enabled = moduleEnabledById[module.id] !== false;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={module.id}
|
|
||||||
className={`rounded-lg border px-4 py-3 transition ${enabled ? 'border-[#d1d9e6] bg-white hover:bg-[#f8fafc]' : 'border-[#cbd5e1] bg-[#f8fafc] opacity-70 hover:bg-[#f1f5f9]'}`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 shrink-0 cursor-pointer rounded border-[#cbd5e1] text-[#2563eb] focus:ring-[#2563eb]"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
const checked = event.target.checked;
|
|
||||||
setModuleEnabledById((current) => ({ ...current, [module.id]: checked }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="grid h-8 w-8 shrink-0 place-items-center rounded-md bg-[#ede9ff] text-[12px] font-black text-[#4f46e5]">{module.sortOrder}</span>
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span className="block truncate text-[14px] font-black text-[#111827]">{module.name}</span>
|
|
||||||
<span className="mt-1 block truncate text-[12px] font-bold text-[#64748b]">{module.category} · {module.folderName}</span>
|
|
||||||
</span>
|
|
||||||
<span className={`shrink-0 rounded-md px-2.5 py-1 text-[11px] font-black ${enabled ? 'bg-[#dcfce7] text-[#15803d]' : 'bg-[#e2e8f0] text-[#64748b]'}`}>
|
|
||||||
{enabled ? '사용' : '미사용'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 border-t border-[#e2e8f0] pt-3">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<span className="text-[12px] font-bold text-[#475569]">기본 파라미터</span>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="inline-flex h-7 cursor-pointer items-center gap-1 rounded-md border border-[#bfdbfe] px-2 text-[12px] font-bold text-[#2563eb] transition hover:bg-[#eff6ff] disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
disabled={!enabled}
|
|
||||||
onClick={() => setModuleDefaultParametersById((current) => ({
|
|
||||||
...current,
|
|
||||||
[module.id]: [...(current[module.id] ?? []), { key: '', value: '' }],
|
|
||||||
}))}
|
|
||||||
>
|
|
||||||
<Plus className="h-3.5 w-3.5" />
|
|
||||||
행 추가
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2 grid gap-2">
|
|
||||||
{(moduleDefaultParametersById[module.id] ?? []).map((parameter, parameterIndex) => (
|
|
||||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_28px] items-center gap-2" key={`${module.id}-${parameterIndex}`}>
|
|
||||||
<input
|
|
||||||
className="h-8 min-w-0 rounded-md border border-[#cbd5e1] bg-white px-2.5 font-mono text-[12px] text-[#111827] outline-none transition focus:border-[#2563eb] focus:ring-2 focus:ring-[#bfdbfe] disabled:cursor-not-allowed disabled:bg-[#f1f5f9]"
|
|
||||||
value={parameter.key}
|
|
||||||
placeholder="키"
|
|
||||||
disabled={!enabled}
|
|
||||||
onChange={(event) => setModuleDefaultParametersById((current) => ({
|
|
||||||
...current,
|
|
||||||
[module.id]: (current[module.id] ?? []).map((item, index) => index === parameterIndex ? { ...item, key: event.target.value } : item),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="h-8 min-w-0 rounded-md border border-[#cbd5e1] bg-white px-2.5 font-mono text-[12px] text-[#111827] outline-none transition focus:border-[#2563eb] focus:ring-2 focus:ring-[#bfdbfe] disabled:cursor-not-allowed disabled:bg-[#f1f5f9]"
|
|
||||||
value={parameter.value}
|
|
||||||
placeholder="값"
|
|
||||||
disabled={!enabled}
|
|
||||||
onChange={(event) => setModuleDefaultParametersById((current) => ({
|
|
||||||
...current,
|
|
||||||
[module.id]: (current[module.id] ?? []).map((item, index) => index === parameterIndex ? { ...item, value: event.target.value } : item),
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title="행 삭제"
|
|
||||||
aria-label="행 삭제"
|
|
||||||
className="grid h-7 w-7 cursor-pointer place-items-center rounded-md text-[#94a3b8] transition hover:bg-[#fef2f2] hover:text-[#dc2626] disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
disabled={!enabled}
|
|
||||||
onClick={() => setModuleDefaultParametersById((current) => ({
|
|
||||||
...current,
|
|
||||||
[module.id]: (current[module.id] ?? []).filter((_, index) => index !== parameterIndex),
|
|
||||||
}))}
|
|
||||||
>
|
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}) : (
|
|
||||||
<p className="rounded-lg border border-dashed border-[#d1d9e6] px-4 py-10 text-center text-[13px] font-bold text-[#64748b]">등록된 모듈이 없습니다.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{(selectedWeek?.environmentProfiles?.length ?? 0) > 0 ? (
|
|
||||||
<section className="border-t border-[#cbd5e1] pt-4">
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[15px] font-black text-[#111827]">환경별 모듈 구성</h3>
|
<h3 className="text-[15px] font-black text-[#111827]">환경별 모듈 구성</h3>
|
||||||
<p className="mt-1 text-[12px] font-bold text-[#64748b]">소스 저장소는 공통으로 유지하고, 로컬과 플랫폼에서 실행할 모듈만 다르게 선택합니다.</p>
|
<p className="mt-1 text-[12px] font-bold text-[#64748b]">소스 저장소는 공통으로 유지하고, 로컬과 플랫폼에서 실행할 모듈만 다르게 선택합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 grid gap-3">
|
<div className="mt-3 grid gap-3">
|
||||||
{(selectedWeek.environmentProfiles ?? []).map((profile) => {
|
{editableEnvironmentProfiles.map((profile) => {
|
||||||
const selectedModuleIds = new Set(environmentModuleIdsByPlatformKey[profile.platformKey] ?? []);
|
const selectedModuleIds = new Set(environmentModuleIdsByPlatformKey[profile.storageKey] ?? (selectedWeek.modules ?? []).map((module) => module.id));
|
||||||
const selectedCount = (selectedWeek.modules ?? []).filter((module) => selectedModuleIds.has(module.id)).length;
|
const selectedCount = (selectedWeek.modules ?? []).filter((module) => selectedModuleIds.has(module.id)).length;
|
||||||
const targetLabel = profile.platformKey === 'futuredrive-local-pc' ? '로컬' : '플랫폼';
|
|
||||||
return (
|
return (
|
||||||
<div key={profile.id} className="rounded-lg border border-[#d1d9e6] bg-[#f8fafc] p-3">
|
<div key={profile.id} className="rounded-lg border border-[#d1d9e6] bg-[#f8fafc] p-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<span className="block truncate text-[13px] font-black text-[#111827]">{profile.name}</span>
|
<span className="block truncate text-[13px] font-black text-[#111827]">{profile.name}</span>
|
||||||
<span className="mt-0.5 block text-[11px] font-bold text-[#64748b]">{targetLabel} · {profile.profileType}</span>
|
<span className="mt-0.5 block text-[11px] font-bold text-[#64748b]">{profile.storageKey === 'local' ? '로컬 실행 환경' : '플랫폼 실행 환경'}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="shrink-0 rounded-md bg-white px-2.5 py-1 text-[11px] font-black text-[#475569]">{selectedCount}/{selectedWeek.modules?.length ?? 0}</span>
|
<span className="shrink-0 rounded-md bg-white px-2.5 py-1 text-[11px] font-black text-[#475569]">{selectedCount}/{selectedWeek.modules?.length ?? 0}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1008,7 +987,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
{(selectedWeek.modules ?? []).map((module) => {
|
{(selectedWeek.modules ?? []).map((module) => {
|
||||||
const globallyEnabled = moduleEnabledById[module.id] !== false;
|
const globallyEnabled = moduleEnabledById[module.id] !== false;
|
||||||
const checked = selectedModuleIds.has(module.id);
|
const checked = selectedModuleIds.has(module.id);
|
||||||
const parameterRows = environmentModuleParametersByPlatformKey[profile.platformKey]?.[module.id] ?? [];
|
const parameterRows = environmentModuleParametersByPlatformKey[profile.storageKey]?.[module.id] ?? [];
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={module.id}
|
key={module.id}
|
||||||
@@ -1023,21 +1002,21 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const enabled = event.target.checked;
|
const enabled = event.target.checked;
|
||||||
setEnvironmentModuleIdsByPlatformKey((current) => {
|
setEnvironmentModuleIdsByPlatformKey((current) => {
|
||||||
const currentIds = new Set(current[profile.platformKey] ?? []);
|
const currentIds = new Set(current[profile.storageKey] ?? (selectedWeek.modules ?? []).map((item) => item.id));
|
||||||
if (enabled) currentIds.add(module.id);
|
if (enabled) currentIds.add(module.id);
|
||||||
else currentIds.delete(module.id);
|
else currentIds.delete(module.id);
|
||||||
return { ...current, [profile.platformKey]: [...currentIds] };
|
return { ...current, [profile.storageKey]: [...currentIds] };
|
||||||
});
|
});
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
setEnvironmentModuleParametersByPlatformKey((current) => {
|
setEnvironmentModuleParametersByPlatformKey((current) => {
|
||||||
const profileParameters = { ...(current[profile.platformKey] ?? {}) };
|
const profileParameters = { ...(current[profile.storageKey] ?? {}) };
|
||||||
delete profileParameters[module.id];
|
delete profileParameters[module.id];
|
||||||
return { ...current, [profile.platformKey]: profileParameters };
|
return { ...current, [profile.storageKey]: profileParameters };
|
||||||
});
|
});
|
||||||
setEnvironmentModuleDockerImagesByPlatformKey((current) => {
|
setEnvironmentModuleDockerImagesByPlatformKey((current) => {
|
||||||
const profileImages = { ...(current[profile.platformKey] ?? {}) };
|
const profileImages = { ...(current[profile.storageKey] ?? {}) };
|
||||||
delete profileImages[module.id];
|
delete profileImages[module.id];
|
||||||
return { ...current, [profile.platformKey]: profileImages };
|
return { ...current, [profile.storageKey]: profileImages };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -1049,13 +1028,13 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
<label className="mb-2 block text-[10px] text-[#64748b]">
|
<label className="mb-2 block text-[10px] text-[#64748b]">
|
||||||
<span className="mb-1 block">실행 이미지</span>
|
<span className="mb-1 block">실행 이미지</span>
|
||||||
<input
|
<input
|
||||||
value={environmentModuleDockerImagesByPlatformKey[profile.platformKey]?.[module.id] ?? ''}
|
value={environmentModuleDockerImagesByPlatformKey[profile.storageKey]?.[module.id] ?? ''}
|
||||||
placeholder={module.dockerImage ?? '기본 이미지 사용'}
|
placeholder={module.dockerImage ?? '기본 이미지 사용'}
|
||||||
className="h-7 w-full rounded border border-[#dbe3ee] px-1.5 font-mono text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
className="h-7 w-full rounded border border-[#dbe3ee] px-1.5 font-mono text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
||||||
onChange={(event) => setEnvironmentModuleDockerImagesByPlatformKey((current) => ({
|
onChange={(event) => setEnvironmentModuleDockerImagesByPlatformKey((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[profile.platformKey]: {
|
[profile.storageKey]: {
|
||||||
...(current[profile.platformKey] ?? {}),
|
...(current[profile.storageKey] ?? {}),
|
||||||
[module.id]: event.target.value,
|
[module.id]: event.target.value,
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
@@ -1069,9 +1048,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
aria-label={`${module.name} 파라미터 추가`}
|
aria-label={`${module.name} 파라미터 추가`}
|
||||||
onClick={() => setEnvironmentModuleParametersByPlatformKey((current) => ({
|
onClick={() => setEnvironmentModuleParametersByPlatformKey((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[profile.platformKey]: {
|
[profile.storageKey]: {
|
||||||
...(current[profile.platformKey] ?? {}),
|
...(current[profile.storageKey] ?? {}),
|
||||||
[module.id]: [...(current[profile.platformKey]?.[module.id] ?? []), { key: '', value: '' }],
|
[module.id]: [...(current[profile.storageKey]?.[module.id] ?? []), { key: '', value: '' }],
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
@@ -1085,9 +1064,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
placeholder="키"
|
placeholder="키"
|
||||||
className="min-w-0 flex-1 rounded border border-[#dbe3ee] px-1.5 py-1 text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
className="min-w-0 flex-1 rounded border border-[#dbe3ee] px-1.5 py-1 text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
||||||
onChange={(event) => setEnvironmentModuleParametersByPlatformKey((current) => {
|
onChange={(event) => setEnvironmentModuleParametersByPlatformKey((current) => {
|
||||||
const rows = [...(current[profile.platformKey]?.[module.id] ?? [])];
|
const rows = [...(current[profile.storageKey]?.[module.id] ?? [])];
|
||||||
rows[parameterIndex] = { ...rows[parameterIndex], key: event.target.value };
|
rows[parameterIndex] = { ...rows[parameterIndex], key: event.target.value };
|
||||||
return { ...current, [profile.platformKey]: { ...(current[profile.platformKey] ?? {}), [module.id]: rows } };
|
return { ...current, [profile.storageKey]: { ...(current[profile.storageKey] ?? {}), [module.id]: rows } };
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
@@ -1095,9 +1074,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
placeholder="값"
|
placeholder="값"
|
||||||
className="min-w-0 flex-1 rounded border border-[#dbe3ee] px-1.5 py-1 text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
className="min-w-0 flex-1 rounded border border-[#dbe3ee] px-1.5 py-1 text-[11px] font-medium outline-none focus:border-[#60a5fa]"
|
||||||
onChange={(event) => setEnvironmentModuleParametersByPlatformKey((current) => {
|
onChange={(event) => setEnvironmentModuleParametersByPlatformKey((current) => {
|
||||||
const rows = [...(current[profile.platformKey]?.[module.id] ?? [])];
|
const rows = [...(current[profile.storageKey]?.[module.id] ?? [])];
|
||||||
rows[parameterIndex] = { ...rows[parameterIndex], value: event.target.value };
|
rows[parameterIndex] = { ...rows[parameterIndex], value: event.target.value };
|
||||||
return { ...current, [profile.platformKey]: { ...(current[profile.platformKey] ?? {}), [module.id]: rows } };
|
return { ...current, [profile.storageKey]: { ...(current[profile.storageKey] ?? {}), [module.id]: rows } };
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -1106,9 +1085,9 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
aria-label={`${module.name} 파라미터 삭제`}
|
aria-label={`${module.name} 파라미터 삭제`}
|
||||||
onClick={() => setEnvironmentModuleParametersByPlatformKey((current) => ({
|
onClick={() => setEnvironmentModuleParametersByPlatformKey((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[profile.platformKey]: {
|
[profile.storageKey]: {
|
||||||
...(current[profile.platformKey] ?? {}),
|
...(current[profile.storageKey] ?? {}),
|
||||||
[module.id]: (current[profile.platformKey]?.[module.id] ?? []).filter((_, index) => index !== parameterIndex),
|
[module.id]: (current[profile.storageKey]?.[module.id] ?? []).filter((_, index) => index !== parameterIndex),
|
||||||
},
|
},
|
||||||
}))}
|
}))}
|
||||||
>
|
>
|
||||||
@@ -1128,63 +1107,15 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
<section className="border-t border-[#cbd5e1] pt-4">
|
</div>
|
||||||
<div className="flex items-center justify-between gap-3">
|
) : activeTab === 'maps' ? (
|
||||||
<div>
|
<div className="grid gap-4 p-5">
|
||||||
<h3 className="text-[15px] font-black text-[#111827]">시각화 플랫폼</h3>
|
<section>
|
||||||
<p className="mt-1 text-[12px] font-bold text-[#64748b]">시각화 화면을 사용할 플랫폼을 선택하세요.</p>
|
<div>
|
||||||
</div>
|
|
||||||
{visualizationPlatforms.length ? (
|
|
||||||
<span className="rounded-md bg-[#f1f5f9] px-3 py-1.5 text-[12px] font-black text-[#475569]">
|
|
||||||
{visualizationPlatforms.filter((platform) => visualizationEnabledByKey[platform.key]).length}/{visualizationPlatforms.length}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="mt-3 grid gap-2">
|
|
||||||
{visualizationPlatforms.length ? visualizationPlatforms.map((platform) => {
|
|
||||||
const enabled = Boolean(visualizationEnabledByKey[platform.key]);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={platform.key}
|
|
||||||
className={`flex cursor-pointer items-center gap-3 rounded-lg border px-4 py-3 transition ${enabled ? 'border-[#d1d9e6] bg-white hover:bg-[#f8fafc]' : 'border-[#cbd5e1] bg-[#f8fafc] opacity-75 hover:bg-[#f1f5f9]'}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-4 w-4 shrink-0 cursor-pointer rounded border-[#cbd5e1] text-[#2563eb] focus:ring-[#2563eb]"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(event) => {
|
|
||||||
const checked = event.target.checked;
|
|
||||||
setVisualizationEnabledByKey((current) => ({ ...current, [platform.key]: checked }));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="min-w-0 flex-1">
|
|
||||||
<span className="block truncate text-[14px] font-black text-[#111827]">{platform.displayName}</span>
|
|
||||||
<span className="mt-1 block truncate text-[12px] font-bold text-[#64748b]">{platform.description ?? '실행 설정 정보 없음'}</span>
|
|
||||||
</span>
|
|
||||||
<span className={`shrink-0 rounded-md px-2.5 py-1 text-[11px] font-black ${enabled ? 'bg-[#dcfce7] text-[#15803d]' : 'bg-[#e2e8f0] text-[#64748b]'}`}>
|
|
||||||
{enabled ? '사용' : '미사용'}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}) : (
|
|
||||||
<p className="rounded-lg border border-dashed border-[#d1d9e6] px-4 py-10 text-center text-[13px] font-bold text-[#64748b]">등록된 시각화 플랫폼이 없습니다.</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section className="border-t border-[#cbd5e1] pt-4">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[15px] font-black text-[#111827]">학습 맵</h3>
|
<h3 className="text-[15px] font-black text-[#111827]">학습 맵</h3>
|
||||||
<p className="mt-1 text-[12px] font-bold text-[#64748b]">학생이 맵 선택 화면에서 사용할 NAV2 학습 환경을 선택하세요.</p>
|
<p className="mt-1 text-[12px] font-bold text-[#64748b]">학생이 맵 선택 화면에서 사용할 NAV2 학습 환경을 선택하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setMapCreateOpen(true)}
|
|
||||||
className="inline-flex h-8 items-center gap-1.5 rounded-md border border-[#d1d9e6] bg-white px-3 text-[12px] font-black text-[#475569] transition hover:border-[#2563eb] hover:bg-[#eff6ff] hover:text-[#2563eb]"
|
|
||||||
>
|
|
||||||
<Plus size={14} aria-hidden="true" />
|
|
||||||
맵 추가
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 grid gap-2">
|
<div className="mt-3 grid gap-2">
|
||||||
{mapOptions.map((map) => {
|
{mapOptions.map((map) => {
|
||||||
@@ -1588,7 +1519,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{mapCreateOpen ? (
|
{mapCreateOpen ? createPortal(
|
||||||
<div className="fixed inset-0 z-50 grid place-items-center bg-slate-950/45 p-5" role="dialog" aria-modal="true" aria-label="학습 맵 추가">
|
<div className="fixed inset-0 z-50 grid place-items-center bg-slate-950/45 p-5" role="dialog" aria-modal="true" aria-label="학습 맵 추가">
|
||||||
<section className="w-full max-w-[420px] rounded-lg border border-[#cbd5e1] bg-white shadow-2xl">
|
<section className="w-full max-w-[420px] rounded-lg border border-[#cbd5e1] bg-white shadow-2xl">
|
||||||
<header className="border-b border-[#e2e8f0] px-5 py-4">
|
<header className="border-b border-[#e2e8f0] px-5 py-4">
|
||||||
@@ -1614,7 +1545,7 @@ export function CurriculumPage({ session, selectedCourseId }: { session: AuthSes
|
|||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
, document.body) : null}
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
open={deleteConfirmOpen && Boolean(selectedWeek)}
|
open={deleteConfirmOpen && Boolean(selectedWeek)}
|
||||||
|
|||||||
Reference in New Issue
Block a user