diff --git a/src/frontend/apps/web/app/(main)/huddle/page.tsx b/src/frontend/apps/web/app/(main)/huddle/page.tsx
new file mode 100644
index 00000000..bb7a7f7b
--- /dev/null
+++ b/src/frontend/apps/web/app/(main)/huddle/page.tsx
@@ -0,0 +1,7 @@
+import HuddleContainer from '@/src/features/video/ui/huddle-container';
+
+//허들 테스트를 위한 임시페이지(삭제예정)
+const HuddleTest = () => {
+ return ;
+};
+export default HuddleTest;
diff --git a/src/frontend/apps/web/src/features/video/.keep b/src/frontend/apps/web/src/features/video/.keep
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/frontend/apps/web/src/features/video/index.ts b/src/frontend/apps/web/src/features/video/index.ts
new file mode 100644
index 00000000..19b757b7
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/index.ts
@@ -0,0 +1 @@
+export { default as HuddleContainer } from './ui/huddle-container';
diff --git a/src/frontend/apps/web/src/features/video/model/huddle-control.type.ts b/src/frontend/apps/web/src/features/video/model/huddle-control.type.ts
new file mode 100644
index 00000000..d0c333a7
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/model/huddle-control.type.ts
@@ -0,0 +1,5 @@
+export enum HuddleControl {
+ Mic = 'mic',
+ Video = 'video',
+ Screen = 'screen',
+}
diff --git a/src/frontend/apps/web/src/features/video/model/huddle-controls-reducer.ts b/src/frontend/apps/web/src/features/video/model/huddle-controls-reducer.ts
new file mode 100644
index 00000000..c0667aea
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/model/huddle-controls-reducer.ts
@@ -0,0 +1,16 @@
+import { type HuddleControl } from './huddle-control.type';
+
+export type HuddleControlsState = {
+ [HuddleControl.Mic]: boolean;
+ [HuddleControl.Video]: boolean;
+ [HuddleControl.Screen]: boolean;
+};
+
+export type HuddleControlAction = {
+ type: HuddleControl;
+};
+
+export const huddleControlReducer = (
+ state: HuddleControlsState,
+ action: HuddleControlAction,
+) => ({ ...state, [action.type]: !state[action.type] });
diff --git a/src/frontend/apps/web/src/features/video/model/index.ts b/src/frontend/apps/web/src/features/video/model/index.ts
new file mode 100644
index 00000000..25cac9ea
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/model/index.ts
@@ -0,0 +1,6 @@
+export { HuddleControl } from './huddle-control.type';
+export {
+ type HuddleControlsState,
+ type HuddleControlAction,
+ huddleControlReducer,
+} from './huddle-controls-reducer';
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-container.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-container.tsx
new file mode 100644
index 00000000..b6a7b042
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-container.tsx
@@ -0,0 +1,20 @@
+import { Headphones } from 'lucide-react';
+import HuddleContent from './huddle-section';
+import ControlsContainer from './huddle-footer';
+
+const HuddleContainer = () => {
+ return (
+
+
+ slack-전체에서의 허들 1명
+
+
+
+
+
+
+
+
+ );
+};
+export default HuddleContainer;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-control-item.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-control-item.tsx
new file mode 100644
index 00000000..18b8c2f6
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-control-item.tsx
@@ -0,0 +1,25 @@
+import { ToggleGroupItem } from '@workspace/ui/components';
+import { HuddleControl, HuddleControlAction } from '../model';
+import { Dispatch } from 'react';
+
+type HuddleControlProps = {
+ value: HuddleControl;
+ children: React.ReactNode;
+ dispatch: Dispatch;
+};
+
+const HuddleControlItem = ({
+ value,
+ children,
+ dispatch,
+}: HuddleControlProps) => {
+ return (
+ dispatch({ type: value })}
+ >
+ {children}
+
+ );
+};
+export default HuddleControlItem;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-controls-group.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-controls-group.tsx
new file mode 100644
index 00000000..5561a6f0
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-controls-group.tsx
@@ -0,0 +1,63 @@
+'use client';
+import { Button, ToggleGroup } from '@workspace/ui/components';
+import {
+ Mic,
+ MicOff,
+ ScreenShare,
+ ScreenShareOff,
+ Video,
+ VideoOff,
+} from 'lucide-react';
+import { useReducer } from 'react';
+import HuddleControlItem from './huddle-control-item';
+import { HuddleControl, huddleControlReducer } from '../model';
+
+export const controlIconList = {
+ [HuddleControl.Mic]: { on: , off: },
+ [HuddleControl.Video]: { on: , off: },
+ [HuddleControl.Screen]: { on: , off: },
+} as const;
+
+const HuddleControlsGroup = () => {
+ const [controlGroupState, controlGroupDispatch] = useReducer(
+ huddleControlReducer,
+ {
+ [HuddleControl.Mic]: false,
+ [HuddleControl.Video]: false,
+ [HuddleControl.Screen]: false,
+ },
+ );
+
+ return (
+
+
+ {Object.entries(controlIconList).map(([key, component]) => {
+ const controlKey = key as HuddleControl;
+ return (
+
+ {component[controlGroupState[controlKey] ? 'on' : 'off']}
+
+ );
+ })}
+
+
+
+
+ );
+};
+export default HuddleControlsGroup;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-footer.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-footer.tsx
new file mode 100644
index 00000000..af728b8f
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-footer.tsx
@@ -0,0 +1,19 @@
+import { Toggle } from '@workspace/ui/components';
+import HuddleControlsGroup from './huddle-controls-group';
+import { MessageSquareText } from 'lucide-react';
+
+const HuddleFooter = () => {
+ return (
+
+
+
+
+
+
+ );
+};
+export default HuddleFooter;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-participant-avatar.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-participant-avatar.tsx
new file mode 100644
index 00000000..3629f1cc
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-participant-avatar.tsx
@@ -0,0 +1,14 @@
+import { Avatar, AvatarFallback, AvatarImage } from '@workspace/ui/components';
+
+const HuddleParticipantAvatar = () => {
+ return (
+
+
+ user profile
+
+ );
+};
+export default HuddleParticipantAvatar;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-participant-card.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-participant-card.tsx
new file mode 100644
index 00000000..b215406f
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-participant-card.tsx
@@ -0,0 +1,11 @@
+import HuddleParticipantAvatar from './huddle-participant-avatar';
+
+const HuddleParticipantCard = () => {
+ // 참여자 카메라 화면 또는 아바타가 보여질 예정
+ return (
+
+
+
+ );
+};
+export default HuddleParticipantCard;
diff --git a/src/frontend/apps/web/src/features/video/ui/huddle-section.tsx b/src/frontend/apps/web/src/features/video/ui/huddle-section.tsx
new file mode 100644
index 00000000..96031fe2
--- /dev/null
+++ b/src/frontend/apps/web/src/features/video/ui/huddle-section.tsx
@@ -0,0 +1,13 @@
+import HuddleParticipant from './huddle-participant-card';
+
+const HuddleSection = () => {
+ return (
+
+
+
+ {/*
+ */}
+
+ );
+};
+export default HuddleSection;
diff --git a/src/frontend/packages/ui/src/components/Toggle/toggle.tsx b/src/frontend/packages/ui/src/components/Toggle/toggle.tsx
index c566f719..b774dff0 100644
--- a/src/frontend/packages/ui/src/components/Toggle/toggle.tsx
+++ b/src/frontend/packages/ui/src/components/Toggle/toggle.tsx
@@ -6,16 +6,16 @@ import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@workspace/ui/lib/utils';
const toggleVariants = cva(
- 'inline-flex items-center justify-center rounded-full text-sm font-medium text-muted-foreground ring-offset-background transition-colors hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 gap-2',
+ 'inline-flex items-center justify-center rounded-full text-sm font-medium text-muted ring-offset-background transition-colors hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-primary [&_svg]:pointer-events-none [&_svg]:w-full [&_svg]:h-full [&_svg]:shrink-0 gap-2',
{
variants: {
variant: {
- default: 'bg-transparent',
- outline: 'border border-input bg-transparent hover:text-accent-foreground',
+ default: 'bg-toggle',
+ outline: 'border border-input hover:text-accent-foreground',
},
size: {
- default: 'w-10 h-10 px-3',
- lg: 'w-12 h-12 px-5',
+ default: 'w-10 h-10 p-3',
+ lg: 'w-14 h-14 p-4',
},
},
defaultVariants: {
@@ -25,15 +25,17 @@ const toggleVariants = cva(
},
);
-const Toggle = React.forwardRef, React.ComponentPropsWithoutRef & VariantProps>(
- ({ className, variant, size, ...props }, ref) => (
-
- ),
-);
+const Toggle = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, variant, size, ...props }, ref) => (
+
+));
Toggle.displayName = TogglePrimitive.Root.displayName;
diff --git a/src/frontend/packages/ui/tailwind.config.ts b/src/frontend/packages/ui/tailwind.config.ts
index 480152d7..3f8fe18d 100644
--- a/src/frontend/packages/ui/tailwind.config.ts
+++ b/src/frontend/packages/ui/tailwind.config.ts
@@ -4,7 +4,11 @@ import { fontFamily } from 'tailwindcss/defaultTheme';
const config = {
darkMode: ['class'],
- content: ['app/**/*.{ts,tsx}', 'src/**/*.{ts,tsx}', '../../packages/ui/src/components/**/*.{ts,tsx}'],
+ content: [
+ 'app/**/*.{ts,tsx}',
+ 'src/**/*.{ts,tsx}',
+ '../../packages/ui/src/components/**/*.{ts,tsx}',
+ ],
theme: {
extend: {
fontFamily: {
@@ -16,6 +20,7 @@ const config = {
live: '#fff4e1',
thread: '#1264a3',
chatboxHover: '#F1F5F9',
+ toggle: '#E59A43',
black: {
100: 'rgba(0, 0, 0, 0.5)',
},