Compare commits
2 Commits
196ba51ec6
...
38ab528d24
| Author | SHA1 | Date | |
|---|---|---|---|
| 38ab528d24 | |||
| 324e644ef6 |
@@ -0,0 +1,13 @@
|
|||||||
|
UPDATE "course_week_modules"
|
||||||
|
SET "prebuilt_artifacts" = jsonb_build_array(
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'build-install',
|
||||||
|
'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst',
|
||||||
|
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst',
|
||||||
|
'checksumSha256', 'a11ac9e920fcbe5c5a54e086b72d453932ece32e0c7976858435c0a0ba73a8f9',
|
||||||
|
'architecture', 'amd64',
|
||||||
|
'required', false,
|
||||||
|
'extractPath', '.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE "folder_name" = '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
UPDATE course_week_modules AS module
|
||||||
|
SET prebuilt_artifacts = (
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
CASE
|
||||||
|
WHEN artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
|
||||||
|
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
|
||||||
|
THEN jsonb_set(artifact, '{architecture}', '"amd64"'::jsonb, true)
|
||||||
|
ELSE artifact
|
||||||
|
END
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
|
||||||
|
)
|
||||||
|
WHERE EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM jsonb_array_elements(module.prebuilt_artifacts) AS artifact
|
||||||
|
WHERE artifact->>'url' LIKE '%/studio-artifacts/modules/lms-nav2-%/1.0.0/prebuilt/%'
|
||||||
|
AND COALESCE(NULLIF(artifact->>'architecture', ''), '') = ''
|
||||||
|
);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
UPDATE "course_environment_profiles"
|
||||||
|
SET "config" = replace(
|
||||||
|
replace(
|
||||||
|
replace(
|
||||||
|
"config"::text,
|
||||||
|
'nav2-slam:humble',
|
||||||
|
'future-nav2-gazebo:humble'
|
||||||
|
),
|
||||||
|
'runtime-images/nav2/humble/latest/nav2-slam-humble.tar.zst',
|
||||||
|
'runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst'
|
||||||
|
),
|
||||||
|
'nav2-slam-humble.tar.zst',
|
||||||
|
'future-nav2-gazebo-humble.tar.zst'
|
||||||
|
)::jsonb
|
||||||
|
WHERE "config"::text LIKE '%nav2-slam:humble%'
|
||||||
|
OR "config"::text LIKE '%nav2-slam-humble.tar.zst%';
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
UPDATE "course_week_modules" AS module
|
||||||
|
SET "prebuilt_artifacts" = (
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
CASE
|
||||||
|
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-simulator-build-install.tar.zst'
|
||||||
|
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-simulator-build-install.tar.zst'
|
||||||
|
THEN jsonb_set(
|
||||||
|
artifact,
|
||||||
|
'{checksumSha256}',
|
||||||
|
to_jsonb('8432100a17d85047ab4ac79445d5eacf236ccc79a785db61d14270104af270b7'::text)
|
||||||
|
)
|
||||||
|
ELSE artifact
|
||||||
|
END
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
|
||||||
|
)
|
||||||
|
WHERE module."folder_name" = '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws';
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
-- The NAV2 viewer expects the unified slam-ros-bridge API
|
||||||
|
-- (/state, /ws/state and /ws/control). The older nav2-ros-bridge context
|
||||||
|
-- does not expose those endpoints, so remove that override from every
|
||||||
|
-- local NAV2 course environment profile.
|
||||||
|
UPDATE "course_environment_profiles"
|
||||||
|
SET
|
||||||
|
"config" = jsonb_set(
|
||||||
|
("config"::jsonb #- '{bridge,buildContext}'),
|
||||||
|
'{bridge,image}',
|
||||||
|
'"slam-ros-bridge:latest"'::jsonb,
|
||||||
|
true
|
||||||
|
),
|
||||||
|
"updated_at" = CURRENT_TIMESTAMP
|
||||||
|
WHERE "platform_key" = 'NAV2'
|
||||||
|
AND (
|
||||||
|
"config"::jsonb #>> '{bridge,buildContext}' = 'resources/nav2-ros-bridge'
|
||||||
|
OR "config"::jsonb #>> '{bridge,image}' = 'nav2-ros-bridge'
|
||||||
|
);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
UPDATE "course_week_modules" AS module
|
||||||
|
SET "prebuilt_artifacts" = (
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
CASE
|
||||||
|
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-planning-build-install.tar.zst'
|
||||||
|
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-planning-build-install.tar.zst'
|
||||||
|
THEN jsonb_set(
|
||||||
|
artifact,
|
||||||
|
'{checksumSha256}',
|
||||||
|
to_jsonb('6087b12b35a4c2ea3073c6b9fad5e43175d7425c2f4a042fa654e8c42b2bf29d'::text)
|
||||||
|
)
|
||||||
|
WHEN artifact->>'fileName' = 'lms-nav2-gazebo-control-build-install.tar.zst'
|
||||||
|
OR artifact->>'url' LIKE '%/lms-nav2-gazebo-control-build-install.tar.zst'
|
||||||
|
THEN jsonb_set(
|
||||||
|
artifact,
|
||||||
|
'{checksumSha256}',
|
||||||
|
to_jsonb('ce8949b8a14f6d29b3924b82d4819923368fb67029f45cc50a07175afa659a7a'::text)
|
||||||
|
)
|
||||||
|
ELSE artifact
|
||||||
|
END
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(module."prebuilt_artifacts") AS artifact
|
||||||
|
)
|
||||||
|
WHERE module."folder_name" IN (
|
||||||
|
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
|
||||||
|
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
UPDATE "course_week_modules" AS module
|
||||||
|
SET "default_parameters" = CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM jsonb_array_elements(module."default_parameters") AS parameter
|
||||||
|
WHERE parameter #>> '{}' LIKE 'localization_mode:=%'
|
||||||
|
) THEN (
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
CASE
|
||||||
|
WHEN parameter #>> '{}' LIKE 'localization_mode:=%'
|
||||||
|
THEN to_jsonb('localization_mode:=amcl'::text)
|
||||||
|
ELSE parameter
|
||||||
|
END
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(module."default_parameters") AS parameter
|
||||||
|
)
|
||||||
|
ELSE COALESCE(module."default_parameters", '[]'::jsonb)
|
||||||
|
|| jsonb_build_array('localization_mode:=amcl')
|
||||||
|
END
|
||||||
|
WHERE module."folder_name" = '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- The selected map determines SLAM/navigation and 2D/3D viewer mode.
|
||||||
|
-- Environment profiles retain only runtime connection configuration.
|
||||||
|
UPDATE "course_environment_profiles"
|
||||||
|
SET
|
||||||
|
"config" = "config" #- '{viewer,mode}',
|
||||||
|
"updated_at" = CURRENT_TIMESTAMP
|
||||||
|
WHERE "config" #> '{viewer,mode}' IS NOT NULL;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
-- SLAM/navigation is selected from the map. Keep one runtime configuration
|
||||||
|
-- for the remote platform instead of separate visualization profiles.
|
||||||
|
DELETE FROM "course_environment_profiles" legacy
|
||||||
|
USING "course_environment_profiles" platform
|
||||||
|
WHERE legacy."course_id" = platform."course_id"
|
||||||
|
AND legacy."week" = platform."week"
|
||||||
|
AND platform."platform_key" = 'PLATFORM'
|
||||||
|
AND legacy."platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
|
||||||
|
|
||||||
|
WITH canonical AS (
|
||||||
|
SELECT id
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
row_number() OVER (
|
||||||
|
PARTITION BY "course_id", "week"
|
||||||
|
ORDER BY CASE "platform_key"
|
||||||
|
WHEN 'REAL_SLAM' THEN 1
|
||||||
|
WHEN 'REAL_NAVIGATION' THEN 2
|
||||||
|
WHEN 'REAL_SLAM_3D' THEN 3
|
||||||
|
END
|
||||||
|
) AS rank
|
||||||
|
FROM "course_environment_profiles"
|
||||||
|
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION')
|
||||||
|
) ranked
|
||||||
|
WHERE rank = 1
|
||||||
|
)
|
||||||
|
UPDATE "course_environment_profiles" profile
|
||||||
|
SET
|
||||||
|
"platform_key" = 'PLATFORM',
|
||||||
|
"profile_type" = 'PLATFORM',
|
||||||
|
"name" = '플랫폼',
|
||||||
|
"config" = jsonb_set(profile."config" #- '{viewer,mode}', '{type}', '"PLATFORM"'::jsonb, true),
|
||||||
|
"updated_at" = CURRENT_TIMESTAMP
|
||||||
|
FROM canonical
|
||||||
|
WHERE profile.id = canonical.id;
|
||||||
|
|
||||||
|
DELETE FROM "course_environment_profiles"
|
||||||
|
WHERE "platform_key" IN ('REAL_SLAM', 'REAL_SLAM_3D', 'REAL_NAVIGATION');
|
||||||
@@ -25,9 +25,6 @@ WHERE course_id IN (SELECT id FROM target_course);
|
|||||||
DELETE FROM course_environment_profiles
|
DELETE FROM course_environment_profiles
|
||||||
WHERE course_id IN (SELECT id FROM target_course);
|
WHERE course_id IN (SELECT id FROM target_course);
|
||||||
|
|
||||||
DELETE FROM course_week_visualization_platforms
|
|
||||||
WHERE course_id IN (SELECT id FROM target_course);
|
|
||||||
|
|
||||||
DELETE FROM course_week_maps
|
DELETE FROM course_week_maps
|
||||||
WHERE course_id IN (SELECT id FROM target_course);
|
WHERE course_id IN (SELECT id FROM target_course);
|
||||||
|
|
||||||
@@ -153,23 +150,23 @@ WHERE w.course_id IN (SELECT id FROM target_course) AND w.week = 2;
|
|||||||
WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS (
|
WITH module_definitions(category, name, folder_name, template_repository_url, prebuilt_artifacts, default_parameters, docker_image) AS (
|
||||||
VALUES
|
VALUES
|
||||||
('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git',
|
('Simulator', 'NAV2-Gazebo', '[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-simulator.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-simulator/1.0.0/prebuilt/latest/lms-nav2-gazebo-simulator-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-simulator-build-install.tar.zst', 'checksumSha256', '7d2cfe092e81ef15a6e55525410d6f30a803d8c81bee20541dd1d7b964b1fe53', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('gui:=false'), 'future-nav2-gazebo:humble'),
|
||||||
('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git',
|
('Vehicle', 'NAV2-TurtleBot3Burger', '[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-vehicle.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-vehicle/1.0.0/prebuilt/latest/lms-nav2-gazebo-vehicle-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-vehicle-build-install.tar.zst', 'checksumSha256', '42e8179172c178978e5ae5d48e189164f6b8a5030fdaa0eba497ca86a97e96bb', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'platform:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||||
('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git',
|
('Map', 'NAV2-Map', '[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-map.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-map/1.0.0/prebuilt/latest/lms-nav2-gazebo-map-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-map-build-install.tar.zst', 'checksumSha256', 'f409ce54c40aeeed8cf3fc06251c2e321887b3ec5f5827dfb74897a62780582f', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||||
('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git',
|
('Localization', 'NAV2-SLAM', '[lms-nav2-gazebo-slam]nav2-slam_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-slam.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-slam/1.0.0/prebuilt/latest/lms-nav2-gazebo-slam-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-slam-build-install.tar.zst', 'checksumSha256', '186f2aa926eb41140b2783507b521083536aee3be716cf55c591a32250ec0fce', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||||
('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git',
|
('Localization', 'NAV2-Localization', '[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-localization.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=slam'), 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-localization/1.0.0/prebuilt/latest/lms-nav2-gazebo-localization-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-localization-build-install.tar.zst', 'checksumSha256', '44c8d50205f001a196870d03f44d435582b96c51eb4445e50cc8db6203b2c0c9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('localization_mode:=amcl'), 'future-nav2-gazebo:humble'),
|
||||||
('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git',
|
('Perception', 'NAV2-Perception', '[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-perception.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-perception/1.0.0/prebuilt/latest/lms-nav2-gazebo-perception-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-perception-build-install.tar.zst', 'checksumSha256', '23c6523d98506caee9e982741c1c806f9f58581aba777af58eadc7cd5fca1432', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true'), 'future-nav2-gazebo:humble'),
|
||||||
('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git',
|
('Planning', 'NAV2-Planning', '[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-planning.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-planning/1.0.0/prebuilt/latest/lms-nav2-gazebo-planning-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-planning-build-install.tar.zst', 'checksumSha256', 'b0c698acf54ef71915e2707868726b998fbdbb2e2ad15fe65cefc931a9afaa78', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), '[]'::jsonb, 'future-nav2-gazebo:humble'),
|
||||||
('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git',
|
('Control', 'NAV2-Control', '[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-gazebo-control.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-gazebo-control/1.0.0/prebuilt/latest/lms-nav2-gazebo-control-build-install.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-gazebo-control-build-install.tar.zst', 'checksumSha256', '63b3b59377c19288f47aa5619fef9b10938f400771df1805d8ca973648ac0ed8', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=true', 'robot:=turtlebot3', 'level:=2d'), 'future-nav2-gazebo:humble'),
|
||||||
('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git',
|
('Vehicle', 'NAV2-MKMini', '[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws', 'http://localhost:3000/templates/lms-nav2-mkmini.git',
|
||||||
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '8b36d3fda6a81a49da140529d9874e99b734d05e01e12a49e719e2b26f8803d9', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'robot:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
|
jsonb_build_array(jsonb_build_object('url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/modules/lms-nav2-mkmini/1.0.0/prebuilt/amd64/lms-nav2-mkmini-build-install-amd64.tar.zst', 'type', 'build-install', 'fileName', 'lms-nav2-mkmini-build-install-amd64.tar.zst', 'checksumSha256', '6094d2aa5188a28153452aec08e28870984b36c1bc0c06fec8091901f6d26856', 'architecture', 'amd64', 'required', false, 'extractPath', '.')), jsonb_build_array('sim_mode:=false', 'platform:=mkmini', 'level:=2d'), 'future-nav2:humble-jetson_orin')
|
||||||
)
|
)
|
||||||
INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image)
|
INSERT INTO course_week_modules (week_id, sort_order, category, name, folder_name, template_repository_url, source_type, prebuilt_artifacts, default_parameters, docker_image)
|
||||||
SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image
|
SELECT w.id, row_number() OVER (PARTITION BY w.id ORDER BY module_definitions.folder_name), category, name, folder_name, template_repository_url, 'GIT', prebuilt_artifacts, default_parameters, docker_image
|
||||||
@@ -177,19 +174,92 @@ FROM course_weeks w
|
|||||||
CROSS JOIN module_definitions
|
CROSS JOIN module_definitions
|
||||||
WHERE w.course_id IN (SELECT id FROM target_course) AND w.week IN (1, 2);
|
WHERE w.course_id IN (SELECT id FROM target_course) AND w.week IN (1, 2);
|
||||||
|
|
||||||
INSERT INTO course_week_visualization_platforms (course_id, week, platform_key, enabled, created_at, updated_at)
|
|
||||||
SELECT target_course.id, weeks.week, platform.platform_key, true, now(), now()
|
|
||||||
FROM target_course
|
|
||||||
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
|
|
||||||
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
|
||||||
|
|
||||||
INSERT INTO course_environment_profiles (course_id, week, platform_key, profile_type, name, config, enabled, created_at, updated_at)
|
INSERT INTO course_environment_profiles (course_id, week, platform_key, profile_type, name, config, enabled, created_at, updated_at)
|
||||||
SELECT target_course.id, weeks.week, platform.platform_key, platform.type,
|
SELECT target_course.id, weeks.week, environment.platform_key, environment.profile_type,
|
||||||
CASE WHEN platform.platform_key = 'NAV2' THEN '로컬' ELSE platform.display_name END,
|
environment.name, environment.config, true, now(), now()
|
||||||
platform.default_config, true, now(), now()
|
|
||||||
FROM target_course
|
FROM target_course
|
||||||
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
|
CROSS JOIN (VALUES (1), (2)) AS weeks(week)
|
||||||
JOIN visualization_platforms platform ON platform.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
CROSS JOIN (
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
'NAV2',
|
||||||
|
'NAV2',
|
||||||
|
'로컬',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'NAV2',
|
||||||
|
'target', 'local',
|
||||||
|
'viewer', jsonb_build_object('stack', 'nav2'),
|
||||||
|
'bridge', jsonb_build_object(
|
||||||
|
'name', 'nav2-ros-bridge',
|
||||||
|
'port', 8765,
|
||||||
|
'image', 'slam-ros-bridge:latest',
|
||||||
|
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
|
||||||
|
),
|
||||||
|
'runtimeImages', jsonb_build_array(jsonb_build_object(
|
||||||
|
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/gazebo/latest/future-nav2-gazebo-humble.tar.zst',
|
||||||
|
'image', 'future-nav2-gazebo:humble',
|
||||||
|
'fileName', 'future-nav2-gazebo-humble.tar.zst',
|
||||||
|
'required', true,
|
||||||
|
'checksumSha256', '54230e039c1fa5dc82615cef3828c5acae7b8bea8e921b70cfdc5351d8301644'
|
||||||
|
))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'PLATFORM',
|
||||||
|
'PLATFORM',
|
||||||
|
'플랫폼',
|
||||||
|
jsonb_build_object(
|
||||||
|
'type', 'PLATFORM',
|
||||||
|
'target', 'remote-jetson',
|
||||||
|
'runtimeKind', 'real',
|
||||||
|
'viewer', jsonb_build_object('stack', 'nav2'),
|
||||||
|
'bridge', jsonb_build_object(
|
||||||
|
'name', 'nav2-ros-bridge',
|
||||||
|
'port', 8765,
|
||||||
|
'image', 'slam-ros-bridge:latest',
|
||||||
|
'env', jsonb_build_object('ROBOT_STACK', 'nav2')
|
||||||
|
),
|
||||||
|
'runtimeImages', jsonb_build_array(
|
||||||
|
jsonb_build_object(
|
||||||
|
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/slam-ros-bridge/latest/slam-ros-bridge-latest.tar.zst',
|
||||||
|
'image', 'slam-ros-bridge:latest',
|
||||||
|
'fileName', 'slam-ros-bridge-latest.tar.zst',
|
||||||
|
'required', true
|
||||||
|
),
|
||||||
|
jsonb_build_object(
|
||||||
|
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
|
||||||
|
'image', 'future-nav2:humble-jetson_orin',
|
||||||
|
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
|
||||||
|
'required', true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) AS environment(platform_key, profile_type, name, config);
|
||||||
|
|
||||||
|
-- Viewer mode belongs to the selected map, not the platform environment.
|
||||||
|
UPDATE course_environment_profiles profile
|
||||||
|
SET config = profile.config::jsonb #- '{viewer,mode}'
|
||||||
|
WHERE profile.course_id IN (SELECT id FROM target_course);
|
||||||
|
|
||||||
|
-- Real-platform modules need the Jetson NAV2 runtime in addition to the
|
||||||
|
-- visualization bridge image. The desktop app downloads this artifact on
|
||||||
|
-- demand before starting a remote module.
|
||||||
|
UPDATE course_environment_profiles profile
|
||||||
|
SET config = jsonb_set(
|
||||||
|
profile.config::jsonb,
|
||||||
|
'{runtimeImages}',
|
||||||
|
COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) || jsonb_build_array(jsonb_build_object(
|
||||||
|
'url', 'https://pub-ca72c40797144462bdb7d200c215d361.r2.dev/studio-artifacts/runtime-images/nav2/jetson-orin/latest/future-nav2-humble-jetson-orin.tar.zst',
|
||||||
|
'image', 'future-nav2:humble-jetson_orin',
|
||||||
|
'fileName', 'future-nav2-humble-jetson-orin.tar.zst',
|
||||||
|
'required', true
|
||||||
|
)),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||||
|
AND profile.platform_key = 'PLATFORM'
|
||||||
|
AND NOT (COALESCE(profile.config::jsonb->'runtimeImages', '[]'::jsonb) @> '[{"image":"future-nav2:humble-jetson_orin"}]'::jsonb);
|
||||||
|
|
||||||
-- Local simulation gets all Gazebo modules. Platform profiles only receive modules applicable to their workflow.
|
-- Local simulation gets all Gazebo modules. Platform profiles only receive modules applicable to their workflow.
|
||||||
UPDATE course_environment_profiles profile
|
UPDATE course_environment_profiles profile
|
||||||
@@ -204,19 +274,16 @@ SET config = profile.config::jsonb || jsonb_build_object('moduleSet', jsonb_buil
|
|||||||
AND week.week = profile.week
|
AND week.week = profile.week
|
||||||
AND (
|
AND (
|
||||||
(profile.platform_key = 'NAV2' AND module.folder_name LIKE '[lms-nav2-gazebo-%')
|
(profile.platform_key = 'NAV2' AND module.folder_name LIKE '[lms-nav2-gazebo-%')
|
||||||
OR (profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION') AND module.folder_name IN (
|
OR (profile.platform_key = 'PLATFORM' AND module.folder_name NOT IN (
|
||||||
'[lms-nav2-mkmini]nav2-mkmini_v1.0.0_ws',
|
'[lms-nav2-gazebo-simulator]nav2-simulator_v1.0.0_ws',
|
||||||
'[lms-nav2-gazebo-map]nav2-map_v1.0.0_ws',
|
'[lms-nav2-gazebo-vehicle]nav2-vehicle_v1.0.0_ws',
|
||||||
'[lms-nav2-gazebo-perception]nav2-perception_v1.0.0_ws',
|
'[lms-nav2-gazebo-gui]nav2-rviz2_v1.0.0_ws'
|
||||||
'[lms-nav2-gazebo-localization]nav2-localization_v1.0.0_ws',
|
|
||||||
'[lms-nav2-gazebo-planning]nav2-planning_v1.0.0_ws',
|
|
||||||
'[lms-nav2-gazebo-control]nav2-control_v1.0.0_ws'
|
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
), '[]'::jsonb)
|
), '[]'::jsonb)
|
||||||
))
|
))
|
||||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||||
AND profile.platform_key IN ('NAV2', 'REAL_SLAM', 'REAL_NAVIGATION');
|
AND profile.platform_key IN ('NAV2', 'PLATFORM');
|
||||||
|
|
||||||
-- Control runs with TurtleBot3/Gazebo defaults locally, but uses MK-mini and the ARM64 image on a platform.
|
-- Control runs with TurtleBot3/Gazebo defaults locally, but uses MK-mini and the ARM64 image on a platform.
|
||||||
UPDATE course_environment_profiles profile
|
UPDATE course_environment_profiles profile
|
||||||
@@ -238,7 +305,30 @@ SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
|
|||||||
) AS control_module
|
) AS control_module
|
||||||
), true)
|
), true)
|
||||||
WHERE profile.course_id IN (SELECT id FROM target_course)
|
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||||
AND profile.platform_key IN ('REAL_SLAM', 'REAL_NAVIGATION');
|
AND profile.platform_key = 'PLATFORM';
|
||||||
|
|
||||||
|
-- The 2D perception module defaults to Gazebo in the course module list.
|
||||||
|
-- Override only that value for the real platform; the other added modules
|
||||||
|
-- retain their own compatible base parameters.
|
||||||
|
UPDATE course_environment_profiles profile
|
||||||
|
SET config = jsonb_set(profile.config::jsonb, '{moduleSet,modules}', (
|
||||||
|
SELECT jsonb_agg(
|
||||||
|
CASE WHEN item->>'id' = perception_module.id::text THEN item || jsonb_build_object(
|
||||||
|
'defaultParameters', jsonb_build_array('sim_mode:=false')
|
||||||
|
) ELSE item END
|
||||||
|
)
|
||||||
|
FROM jsonb_array_elements(profile.config->'moduleSet'->'modules') AS item
|
||||||
|
CROSS JOIN LATERAL (
|
||||||
|
SELECT module.id
|
||||||
|
FROM course_weeks week
|
||||||
|
JOIN course_week_modules module ON module.week_id = week.id
|
||||||
|
WHERE week.course_id = profile.course_id
|
||||||
|
AND week.week = profile.week
|
||||||
|
AND module.name = 'NAV2-Perception'
|
||||||
|
) AS perception_module
|
||||||
|
), true)
|
||||||
|
WHERE profile.course_id IN (SELECT id FROM target_course)
|
||||||
|
AND profile.platform_key = 'PLATFORM';
|
||||||
|
|
||||||
INSERT INTO course_week_maps (id, course_id, week, map_key, sort_order, created_at, updated_at)
|
INSERT INTO course_week_maps (id, course_id, week, map_key, sort_order, created_at, updated_at)
|
||||||
SELECT gen_random_uuid(), target_course.id, weeks.week, map.map_key, map.sort_order, now(), now()
|
SELECT gen_random_uuid(), target_course.id, weeks.week, map.map_key, map.sort_order, now(), now()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Query, Req } from '@nestjs/
|
|||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
import { ChatService } from './chat.service';
|
import { ChatService } from './chat.service';
|
||||||
|
import { CancelActiveAiResponseDto } from './dto/cancel-active-ai-response.dto';
|
||||||
import { CreateAiResponseDto } from './dto/create-ai-response.dto';
|
import { CreateAiResponseDto } from './dto/create-ai-response.dto';
|
||||||
import { CreateChatMessageDto } from './dto/create-chat-message.dto';
|
import { CreateChatMessageDto } from './dto/create-chat-message.dto';
|
||||||
import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto';
|
import { CreateInstructorChatMessageDto } from './dto/create-instructor-chat-message.dto';
|
||||||
@@ -64,10 +65,11 @@ export class ChatController {
|
|||||||
@Post('ai/cancel-active')
|
@Post('ai/cancel-active')
|
||||||
cancelActiveAiResponse(
|
cancelActiveAiResponse(
|
||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@Body() body: { week?: number },
|
@Body() body: CancelActiveAiResponseDto,
|
||||||
) {
|
) {
|
||||||
return this.chatService.cancelActiveAiResponse(
|
return this.chatService.cancelActiveAiResponse(
|
||||||
this.getBearerToken(request),
|
this.getBearerToken(request),
|
||||||
|
body.courseId,
|
||||||
body.week,
|
body.week,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ type FutureDriveLlmChatResponse = {
|
|||||||
|
|
||||||
const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20;
|
const STUDENT_CHAT_VISIBLE_MESSAGE_LIMIT = 20;
|
||||||
const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6;
|
const AI_CHAT_CONTEXT_MESSAGE_LIMIT = 6;
|
||||||
|
const PUBLIC_GUIDE_COURSE_SLUG = 'autonomy-studio-lms-guide';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ChatService {
|
export class ChatService {
|
||||||
@@ -79,7 +80,7 @@ export class ChatService {
|
|||||||
|
|
||||||
async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
|
async listThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
|
||||||
const student = await this.getLmsUser(accessToken);
|
const student = await this.getLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getStudentCourse(student.id, query.courseId);
|
||||||
const week = query.week
|
const week = query.week
|
||||||
? await this.getCourseWeek(course.id, query.week)
|
? await this.getCourseWeek(course.id, query.week)
|
||||||
: null;
|
: null;
|
||||||
@@ -122,7 +123,7 @@ export class ChatService {
|
|||||||
|
|
||||||
async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
|
async clearStudentThreads(accessToken: string | undefined, query: ListChatThreadsDto) {
|
||||||
const student = await this.getLmsUser(accessToken);
|
const student = await this.getLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getStudentCourse(student.id, query.courseId);
|
||||||
const week = query.week
|
const week = query.week
|
||||||
? await this.getCourseWeek(course.id, query.week)
|
? await this.getCourseWeek(course.id, query.week)
|
||||||
: null;
|
: null;
|
||||||
@@ -167,7 +168,7 @@ export class ChatService {
|
|||||||
dto: CreateChatMessageDto,
|
dto: CreateChatMessageDto,
|
||||||
) {
|
) {
|
||||||
const student = await this.getLmsUser(accessToken);
|
const student = await this.getLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getStudentCourse(student.id, dto.courseId);
|
||||||
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
|
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
|
||||||
const thread = await this.getOrCreateThread({
|
const thread = await this.getOrCreateThread({
|
||||||
courseId: course.id,
|
courseId: course.id,
|
||||||
@@ -220,7 +221,7 @@ export class ChatService {
|
|||||||
dto: CreateAiResponseDto,
|
dto: CreateAiResponseDto,
|
||||||
) {
|
) {
|
||||||
const student = await this.getLmsUser(accessToken);
|
const student = await this.getLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getStudentCourse(student.id, dto.courseId);
|
||||||
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
|
const week = dto.week ? await this.getCourseWeek(course.id, dto.week) : null;
|
||||||
const thread = await this.getOrCreateThread({
|
const thread = await this.getOrCreateThread({
|
||||||
courseId: course.id,
|
courseId: course.id,
|
||||||
@@ -425,10 +426,11 @@ export class ChatService {
|
|||||||
|
|
||||||
async cancelActiveAiResponse(
|
async cancelActiveAiResponse(
|
||||||
accessToken: string | undefined,
|
accessToken: string | undefined,
|
||||||
|
courseId: string,
|
||||||
weekNumber?: number,
|
weekNumber?: number,
|
||||||
) {
|
) {
|
||||||
const student = await this.getLmsUser(accessToken);
|
const student = await this.getLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getStudentCourse(student.id, courseId);
|
||||||
const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null;
|
const week = weekNumber ? await this.getCourseWeek(course.id, weekNumber) : null;
|
||||||
const message = await this.prisma.chatMessage.findFirst({
|
const message = await this.prisma.chatMessage.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -487,7 +489,7 @@ export class ChatService {
|
|||||||
query: ListChatThreadsDto,
|
query: ListChatThreadsDto,
|
||||||
) {
|
) {
|
||||||
const instructor = await this.getInstructorLmsUser(accessToken);
|
const instructor = await this.getInstructorLmsUser(accessToken);
|
||||||
const course = await this.getActiveCourse();
|
const course = await this.getInstructorCourse(instructor.id, query.courseId);
|
||||||
const week = query.week
|
const week = query.week
|
||||||
? await this.getCourseWeek(course.id, query.week)
|
? await this.getCourseWeek(course.id, query.week)
|
||||||
: null;
|
: null;
|
||||||
@@ -680,11 +682,41 @@ export class ChatService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getActiveCourse() {
|
private async getStudentCourse(studentId: string, courseId?: string) {
|
||||||
|
if (!courseId) throw new NotFoundException('Course not found');
|
||||||
|
|
||||||
const course = await this.prisma.course.findFirst({
|
const course = await this.prisma.course.findFirst({
|
||||||
where: { slug: 'futuredrive-autonomy-basic', isActive: true },
|
where: {
|
||||||
|
id: courseId,
|
||||||
|
isActive: true,
|
||||||
|
OR: [
|
||||||
|
{ slug: PUBLIC_GUIDE_COURSE_SLUG },
|
||||||
|
{
|
||||||
|
enrollments: {
|
||||||
|
some: {
|
||||||
|
userId: studentId,
|
||||||
|
status: 'ACTIVE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!course) throw new NotFoundException('Curriculum course not found');
|
if (!course) throw new NotFoundException('Course not found');
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getInstructorCourse(instructorId: string, courseId?: string) {
|
||||||
|
if (!courseId) throw new NotFoundException('Course not found');
|
||||||
|
|
||||||
|
const course = await this.prisma.course.findFirst({
|
||||||
|
where: {
|
||||||
|
id: courseId,
|
||||||
|
isActive: true,
|
||||||
|
teacherId: instructorId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!course) throw new NotFoundException('Course not found');
|
||||||
return course;
|
return course;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
|
export class CancelActiveAiResponseDto {
|
||||||
|
@IsUUID()
|
||||||
|
courseId: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
week?: number;
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
import { IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class CreateAiResponseDto {
|
export class CreateAiResponseDto {
|
||||||
|
@IsUUID()
|
||||||
|
courseId: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
content: string;
|
content: string;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { IsIn, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
import { IsIn, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class CreateChatMessageDto {
|
export class CreateChatMessageDto {
|
||||||
|
@IsUUID()
|
||||||
|
courseId: string;
|
||||||
|
|
||||||
@IsIn(['ai', 'teacher'])
|
@IsIn(['ai', 'teacher'])
|
||||||
mode: 'ai' | 'teacher';
|
mode: 'ai' | 'teacher';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsBoolean, IsIn, IsInt, IsOptional, Max, Min } from 'class-validator';
|
import { IsBoolean, IsIn, IsInt, IsOptional, IsUUID, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class ListChatThreadsDto {
|
export class ListChatThreadsDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
courseId?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsIn(['ai', 'teacher'])
|
@IsIn(['ai', 'teacher'])
|
||||||
mode?: 'ai' | 'teacher';
|
mode?: 'ai' | 'teacher';
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateInitialInstructorCourseDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(120)
|
||||||
|
title!: string;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { InstructorService } from './instructor.service';
|
|||||||
import { AssignInstructorPlatformDto } from './dto/assign-instructor-platform.dto';
|
import { AssignInstructorPlatformDto } from './dto/assign-instructor-platform.dto';
|
||||||
import { CreateInstructorPlatformDto } from './dto/create-instructor-platform.dto';
|
import { CreateInstructorPlatformDto } from './dto/create-instructor-platform.dto';
|
||||||
import { CreateInstructorCourseMapDto } from './dto/create-instructor-course-map.dto';
|
import { CreateInstructorCourseMapDto } from './dto/create-instructor-course-map.dto';
|
||||||
|
import { CreateInitialInstructorCourseDto } from './dto/create-initial-instructor-course.dto';
|
||||||
import { CreateInstructorStudentDto } from './dto/create-instructor-student.dto';
|
import { CreateInstructorStudentDto } from './dto/create-instructor-student.dto';
|
||||||
import { SaveInstructorGroupDto } from './dto/save-instructor-group.dto';
|
import { SaveInstructorGroupDto } from './dto/save-instructor-group.dto';
|
||||||
import { SaveSubmissionScoreDto } from './dto/save-submission-score.dto';
|
import { SaveSubmissionScoreDto } from './dto/save-submission-score.dto';
|
||||||
@@ -26,6 +27,11 @@ export class InstructorController {
|
|||||||
return this.instructorService.listCourses(this.getBearerToken(request));
|
return this.instructorService.listCourses(this.getBearerToken(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('courses/initial')
|
||||||
|
createInitialCourse(@Req() request: Request, @Body() body: CreateInitialInstructorCourseDto) {
|
||||||
|
return this.instructorService.createInitialCourse(this.getBearerToken(request), body.title);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('submissions')
|
@Get('submissions')
|
||||||
listSubmissions(@Req() request: Request) {
|
listSubmissions(@Req() request: Request) {
|
||||||
return this.instructorService.listSubmissions(this.getBearerToken(request));
|
return this.instructorService.listSubmissions(this.getBearerToken(request));
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ import { NAV2_SYSTEM_MAPS } from '../curriculum/course-maps';
|
|||||||
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
|
const INSTRUCTOR_ROLES = new Set(['INSTRUCTOR', 'LMS_ADMIN']);
|
||||||
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
|
const DEFAULT_SUBMISSION_BOT_EMAIL = 'lms-bot@futuredrive.net';
|
||||||
const COURSE_REFERENCE_UPLOAD_ROOT = join(process.cwd(), 'uploads', 'course-references');
|
const COURSE_REFERENCE_UPLOAD_ROOT = join(process.cwd(), 'uploads', 'course-references');
|
||||||
|
const BASE_COURSE_SLUGS = ['futuredrive-autonomy-basic', 'yeungnam-autonomy-basic'];
|
||||||
|
|
||||||
|
function copyJson(value: Prisma.JsonValue): Prisma.InputJsonValue | Prisma.JsonNullValueInput {
|
||||||
|
return value === null ? Prisma.JsonNull : JSON.parse(JSON.stringify(value)) as Prisma.InputJsonValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function remapProfileModuleIds(value: Prisma.JsonValue, moduleIds: Map<string, string>) {
|
||||||
|
const config = copyJson(value);
|
||||||
|
if (!config || typeof config !== 'object' || Array.isArray(config)) return config;
|
||||||
|
|
||||||
|
const moduleSet = (config as Record<string, unknown>).moduleSet;
|
||||||
|
if (!moduleSet || typeof moduleSet !== 'object' || Array.isArray(moduleSet)) return config;
|
||||||
|
|
||||||
|
const modules = (moduleSet as Record<string, unknown>).modules;
|
||||||
|
if (!Array.isArray(modules)) return config;
|
||||||
|
|
||||||
|
for (const module of modules) {
|
||||||
|
if (!module || typeof module !== 'object' || Array.isArray(module)) continue;
|
||||||
|
const id = (module as Record<string, unknown>).id;
|
||||||
|
if (typeof id === 'string' && moduleIds.has(id)) {
|
||||||
|
(module as Record<string, unknown>).id = moduleIds.get(id)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
type SubmissionCommitFile = {
|
type SubmissionCommitFile = {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -3612,6 +3637,152 @@ export class InstructorService {
|
|||||||
return { authUser: user, instructor };
|
return { authUser: user, instructor };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createInitialCourse(accessToken: string | undefined, title: string) {
|
||||||
|
const { authUser, instructor } = await this.getInstructor(accessToken);
|
||||||
|
const organizationName = authUser.primaryAffiliation?.organizationName?.trim();
|
||||||
|
if (!organizationName) throw new BadRequestException('강사 소속 조직을 확인할 수 없습니다.');
|
||||||
|
|
||||||
|
const course = await this.createBaseCourseForInstructor(instructor.id, organizationName, title);
|
||||||
|
return {
|
||||||
|
course: {
|
||||||
|
id: course.id,
|
||||||
|
slug: course.slug,
|
||||||
|
title: course.title,
|
||||||
|
subtitle: course.subtitle,
|
||||||
|
description: course.description,
|
||||||
|
organizationName: course.organizationName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createBaseCourseForInstructor(instructorId: string, organizationName: string, title: string) {
|
||||||
|
const courseTitle = title.trim();
|
||||||
|
if (!courseTitle) throw new BadRequestException('강좌명을 입력해 주세요.');
|
||||||
|
|
||||||
|
const existingCourse = await this.prisma.course.findFirst({
|
||||||
|
where: { teacherId: instructorId, isActive: true },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (existingCourse) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||||
|
|
||||||
|
const template = await this.prisma.course.findFirst({
|
||||||
|
where: { slug: { in: BASE_COURSE_SLUGS }, isActive: true },
|
||||||
|
include: {
|
||||||
|
weeks: {
|
||||||
|
orderBy: { week: 'asc' },
|
||||||
|
include: {
|
||||||
|
topics: { orderBy: { sortOrder: 'asc' } },
|
||||||
|
practices: { orderBy: { sortOrder: 'asc' } },
|
||||||
|
references: { orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }] },
|
||||||
|
modules: { orderBy: { sortOrder: 'asc' } },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
environmentProfiles: { orderBy: [{ week: 'asc' }, { platformKey: 'asc' }] },
|
||||||
|
maps: { orderBy: { key: 'asc' } },
|
||||||
|
weekMaps: { orderBy: [{ week: 'asc' }, { sortOrder: 'asc' }] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!template) {
|
||||||
|
this.logger.error('Base NAV2 course was not found; skipping instructor course initialization.');
|
||||||
|
throw new NotFoundException('베이스 강좌를 찾을 수 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseSlug = `nav2-autonomy-${this.slug(instructorId).slice(0, 12)}`;
|
||||||
|
return this.prisma.$transaction(async (tx) => {
|
||||||
|
const courseAlreadyCreated = await tx.course.findFirst({
|
||||||
|
where: { teacherId: instructorId, isActive: true },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (courseAlreadyCreated) throw new ConflictException('이미 생성된 강좌가 있습니다.');
|
||||||
|
|
||||||
|
const course = await tx.course.create({
|
||||||
|
data: {
|
||||||
|
slug: courseSlug,
|
||||||
|
title: courseTitle,
|
||||||
|
subtitle: template.subtitle,
|
||||||
|
description: template.description,
|
||||||
|
organizationName,
|
||||||
|
teacherId: instructorId,
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const moduleIds = new Map<string, string>();
|
||||||
|
for (const sourceWeek of template.weeks) {
|
||||||
|
const week = await tx.courseWeek.create({
|
||||||
|
data: {
|
||||||
|
courseId: course.id,
|
||||||
|
week: sourceWeek.week,
|
||||||
|
title: sourceWeek.title,
|
||||||
|
tags: sourceWeek.tags,
|
||||||
|
progress: sourceWeek.progress,
|
||||||
|
state: sourceWeek.state,
|
||||||
|
duration: sourceWeek.duration,
|
||||||
|
github: sourceWeek.github,
|
||||||
|
overview: sourceWeek.overview,
|
||||||
|
deliverable: sourceWeek.deliverable,
|
||||||
|
requiresSourceSubmission: sourceWeek.requiresSourceSubmission,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
tx.courseWeekTopic.createMany({ data: sourceWeek.topics.map((topic) => ({ weekId: week.id, title: topic.title, detail: topic.detail, sortOrder: topic.sortOrder })) }),
|
||||||
|
tx.courseWeekPractice.createMany({ data: sourceWeek.practices.map((practice) => ({ weekId: week.id, title: practice.title, description: practice.description, sortOrder: practice.sortOrder })) }),
|
||||||
|
tx.courseWeekReference.createMany({ data: sourceWeek.references.map((reference) => ({ weekId: week.id, kind: reference.kind, title: reference.title, originalFileName: reference.originalFileName, storedFileName: reference.storedFileName, contentType: reference.contentType, fileSizeBytes: reference.fileSizeBytes, sortOrder: reference.sortOrder })) }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const sourceModule of sourceWeek.modules) {
|
||||||
|
const module = await tx.courseWeekModule.create({
|
||||||
|
data: {
|
||||||
|
weekId: week.id,
|
||||||
|
sortOrder: sourceModule.sortOrder,
|
||||||
|
category: sourceModule.category,
|
||||||
|
name: sourceModule.name,
|
||||||
|
folderName: sourceModule.folderName,
|
||||||
|
templateRepositoryUrl: sourceModule.templateRepositoryUrl,
|
||||||
|
sourceType: sourceModule.sourceType,
|
||||||
|
prebuiltArtifacts: copyJson(sourceModule.prebuiltArtifacts),
|
||||||
|
defaultParameters: copyJson(sourceModule.defaultParameters),
|
||||||
|
dockerImage: sourceModule.dockerImage,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
moduleIds.set(sourceModule.id, module.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
...template.environmentProfiles.map((profile) => tx.courseEnvironmentProfile.create({
|
||||||
|
data: {
|
||||||
|
courseId: course.id,
|
||||||
|
week: profile.week,
|
||||||
|
platformKey: profile.platformKey,
|
||||||
|
profileType: profile.profileType,
|
||||||
|
name: profile.name,
|
||||||
|
config: remapProfileModuleIds(profile.config, moduleIds),
|
||||||
|
enabled: profile.enabled,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...template.maps.map((map) => tx.courseMap.create({
|
||||||
|
data: {
|
||||||
|
courseId: course.id,
|
||||||
|
key: map.key,
|
||||||
|
name: map.name,
|
||||||
|
description: map.description,
|
||||||
|
platformKey: map.platformKey,
|
||||||
|
worldFile: map.worldFile,
|
||||||
|
viewerModel: map.viewerModel,
|
||||||
|
layout: copyJson(map.layout),
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...template.weekMaps.map((weekMap) => tx.courseWeekMap.create({
|
||||||
|
data: { courseId: course.id, week: weekMap.week, mapKey: weekMap.mapKey, sortOrder: weekMap.sortOrder },
|
||||||
|
})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return course;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private toUserSyncData(user: AuthResponseUser) {
|
private toUserSyncData(user: AuthResponseUser) {
|
||||||
return {
|
return {
|
||||||
authUserId: user.id,
|
authUserId: user.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user