From 1d8d0852b36d07436e2b813332defbae7f656b4d Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Thu, 8 Jan 2026 02:16:44 +0900 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8feat:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=EC=84=B1=20#68?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hero 컴포넌트 작성 - Card 컴포넌트 작성 - Hero 와 Card 를 바탕으로 메인 페이지 구성 --- apps/client/src/App.tsx | 9 ++- apps/client/src/pages/home/HomePage.tsx | 41 ++++++++++ .../src/pages/home/cards/CreateRoomCard.tsx | 26 +++++++ .../src/pages/home/cards/FeatureCard.tsx | 69 +++++++++++++++++ .../src/pages/home/cards/JoinRoomCard.tsx | 26 +++++++ apps/client/src/pages/home/sections/Hero.tsx | 20 +++++ apps/client/src/shared/ui/card.tsx | 75 +++++++++++++++++++ 7 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 apps/client/src/pages/home/HomePage.tsx create mode 100644 apps/client/src/pages/home/cards/CreateRoomCard.tsx create mode 100644 apps/client/src/pages/home/cards/FeatureCard.tsx create mode 100644 apps/client/src/pages/home/cards/JoinRoomCard.tsx create mode 100644 apps/client/src/pages/home/sections/Hero.tsx create mode 100644 apps/client/src/shared/ui/card.tsx diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index c2a1157..006de1f 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -1,14 +1,15 @@ -import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; import RoomPage from "@/pages/room/RoomPage"; import NotFoundPage from "@/pages/not-found/NotFoundPage"; +import HomePage from "@/pages/home/HomePage"; function App() { return ( - } /> - } /> - } /> + } /> + } /> + } /> ); diff --git a/apps/client/src/pages/home/HomePage.tsx b/apps/client/src/pages/home/HomePage.tsx new file mode 100644 index 0000000..64719f5 --- /dev/null +++ b/apps/client/src/pages/home/HomePage.tsx @@ -0,0 +1,41 @@ +import { CodeXml, Zap, UserCog } from "lucide-react"; +import { Hero } from "./sections/Hero"; +import { CreateRoomCard } from "./cards/CreateRoomCard"; +import { JoinRoomCard } from "./cards/JoinRoomCard"; +import { FeatureCard } from "./cards/FeatureCard"; + +export default function MainPage() { + return ( +
+
+ + +
+ + +
+ +
+ + + +
+
+
+ ); +} diff --git a/apps/client/src/pages/home/cards/CreateRoomCard.tsx b/apps/client/src/pages/home/cards/CreateRoomCard.tsx new file mode 100644 index 0000000..890bdad --- /dev/null +++ b/apps/client/src/pages/home/cards/CreateRoomCard.tsx @@ -0,0 +1,26 @@ +import { Users, ChevronRight } from "lucide-react"; +import { Button } from "@/shared/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/ui/card"; + +export function CreateRoomCard() { + return ( + + +
+ +
+ 방 만들기 + + 새로운 협업 공간을 생성하고 팀원들을 초대하세요 + +
+ +
+ ); +} diff --git a/apps/client/src/pages/home/cards/FeatureCard.tsx b/apps/client/src/pages/home/cards/FeatureCard.tsx new file mode 100644 index 0000000..1c29d09 --- /dev/null +++ b/apps/client/src/pages/home/cards/FeatureCard.tsx @@ -0,0 +1,69 @@ +import type { LucideIcon } from "lucide-react"; +import { Card, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card"; + +interface FeatureCardProps { + icon: LucideIcon; + title: string; + description: string; + colorScheme: "blue" | "green" | "purple" | "orange" | "red"; +} + +const colorClasses = { + blue: { + cardBg: "bg-blue-50", + iconBg: "bg-blue-100", + borderColor: "border-blue-200", + iconColor: "text-blue-500", + }, + green: { + cardBg: "bg-green-50", + iconBg: "bg-green-100", + borderColor: "border-green-200", + iconColor: "text-green-500", + }, + purple: { + cardBg: "bg-purple-50", + iconBg: "bg-purple-100", + borderColor: "border-purple-200", + iconColor: "text-purple-500", + }, + orange: { + cardBg: "bg-orange-50", + iconBg: "bg-orange-100", + borderColor: "border-orange-200", + iconColor: "text-orange-500", + }, + red: { + cardBg: "bg-red-50", + iconBg: "bg-red-100", + borderColor: "border-red-200", + iconColor: "text-red-500", + }, +}; + +export function FeatureCard({ + icon: Icon, + title, + description, + colorScheme, +}: FeatureCardProps) { + const colors = colorClasses[colorScheme]; + + return ( + + +
+
+ +
+
+ {title} + + {description} + +
+
+ ); +} diff --git a/apps/client/src/pages/home/cards/JoinRoomCard.tsx b/apps/client/src/pages/home/cards/JoinRoomCard.tsx new file mode 100644 index 0000000..347bf33 --- /dev/null +++ b/apps/client/src/pages/home/cards/JoinRoomCard.tsx @@ -0,0 +1,26 @@ +import { Hash } from "lucide-react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/ui/card"; + +export function JoinRoomCard() { + return ( + + +
+ +
+ 방 번호로 입장 + + 기존 방 번호를 입력하여 협업에 참여하세요 + +
+ +
+ ); +} diff --git a/apps/client/src/pages/home/sections/Hero.tsx b/apps/client/src/pages/home/sections/Hero.tsx new file mode 100644 index 0000000..4c8f5f6 --- /dev/null +++ b/apps/client/src/pages/home/sections/Hero.tsx @@ -0,0 +1,20 @@ +import logoAnimation from "@/assets/logo_animation.svg"; + +export function Hero() { + return ( +
+
+ CodeJam Logo +
+

+ Code + Jam +

+
+
+

+ 로그인 없이 바로 시작하는 실시간 협업 코드 에디터 +

+
+ ); +} diff --git a/apps/client/src/shared/ui/card.tsx b/apps/client/src/shared/ui/card.tsx new file mode 100644 index 0000000..a103bc9 --- /dev/null +++ b/apps/client/src/shared/ui/card.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; + +import { cn } from "@/shared/lib/utils"; + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardTitle({ className, ...props }: React.ComponentProps<"h3">) { + return ( +

+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +

+ ); +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +

+ ); +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardDescription, + CardContent, +}; From fab1f4331be2846f6d7d685db8959b840f872f0a Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Thu, 8 Jan 2026 03:37:18 +0900 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor:=20ActionCard=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/pages/home/HomePage.tsx | 25 +++++--- .../src/pages/home/cards/ActionCard.tsx | 47 ++++++++++++++ .../src/pages/home/cards/CreateRoomCard.tsx | 26 -------- .../src/pages/home/cards/FeatureCard.tsx | 40 ++---------- .../src/pages/home/cards/JoinRoomCard.tsx | 26 -------- .../home/constants/card-color-schemes.ts | 61 +++++++++++++++++++ 6 files changed, 129 insertions(+), 96 deletions(-) create mode 100644 apps/client/src/pages/home/cards/ActionCard.tsx delete mode 100644 apps/client/src/pages/home/cards/CreateRoomCard.tsx delete mode 100644 apps/client/src/pages/home/cards/JoinRoomCard.tsx create mode 100644 apps/client/src/pages/home/constants/card-color-schemes.ts diff --git a/apps/client/src/pages/home/HomePage.tsx b/apps/client/src/pages/home/HomePage.tsx index 64719f5..7ef0bec 100644 --- a/apps/client/src/pages/home/HomePage.tsx +++ b/apps/client/src/pages/home/HomePage.tsx @@ -1,7 +1,6 @@ -import { CodeXml, Zap, UserCog } from "lucide-react"; +import { Users, Hash, CodeXml, Zap, UserCog } from "lucide-react"; import { Hero } from "./sections/Hero"; -import { CreateRoomCard } from "./cards/CreateRoomCard"; -import { JoinRoomCard } from "./cards/JoinRoomCard"; +import { ActionCard } from "./cards/ActionCard"; import { FeatureCard } from "./cards/FeatureCard"; export default function MainPage() { @@ -11,8 +10,18 @@ export default function MainPage() {
- - + +
@@ -20,19 +29,19 @@ export default function MainPage() { icon={Zap} title="실시간 동기화" description="끊김 없고 빠른 실시간 협업" - colorScheme="red" + colorKey="red" />
diff --git a/apps/client/src/pages/home/cards/ActionCard.tsx b/apps/client/src/pages/home/cards/ActionCard.tsx new file mode 100644 index 0000000..6321e71 --- /dev/null +++ b/apps/client/src/pages/home/cards/ActionCard.tsx @@ -0,0 +1,47 @@ +import type { LucideIcon } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/ui/card"; +import { cardColorSchemes } from "../constants/card-color-schemes"; + +interface ActionCardProps { + icon: LucideIcon; + title: string; + description: string; + colorKey: string; + onClick?: () => void; +} + +export function ActionCard({ + icon: Icon, + title, + description, + colorKey, + onClick, +}: ActionCardProps) { + const colors = cardColorSchemes[colorKey]; + + return ( + + +
+ +
+ {title} + + {description} + +
+ +
+ ); +} diff --git a/apps/client/src/pages/home/cards/CreateRoomCard.tsx b/apps/client/src/pages/home/cards/CreateRoomCard.tsx deleted file mode 100644 index 890bdad..0000000 --- a/apps/client/src/pages/home/cards/CreateRoomCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Users, ChevronRight } from "lucide-react"; -import { Button } from "@/shared/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/shared/ui/card"; - -export function CreateRoomCard() { - return ( - - -
- -
- 방 만들기 - - 새로운 협업 공간을 생성하고 팀원들을 초대하세요 - -
- -
- ); -} diff --git a/apps/client/src/pages/home/cards/FeatureCard.tsx b/apps/client/src/pages/home/cards/FeatureCard.tsx index 1c29d09..c345898 100644 --- a/apps/client/src/pages/home/cards/FeatureCard.tsx +++ b/apps/client/src/pages/home/cards/FeatureCard.tsx @@ -1,53 +1,21 @@ import type { LucideIcon } from "lucide-react"; import { Card, CardDescription, CardHeader, CardTitle } from "@/shared/ui/card"; +import { cardColorSchemes } from "../constants/card-color-schemes"; interface FeatureCardProps { icon: LucideIcon; title: string; description: string; - colorScheme: "blue" | "green" | "purple" | "orange" | "red"; + colorKey: string; } -const colorClasses = { - blue: { - cardBg: "bg-blue-50", - iconBg: "bg-blue-100", - borderColor: "border-blue-200", - iconColor: "text-blue-500", - }, - green: { - cardBg: "bg-green-50", - iconBg: "bg-green-100", - borderColor: "border-green-200", - iconColor: "text-green-500", - }, - purple: { - cardBg: "bg-purple-50", - iconBg: "bg-purple-100", - borderColor: "border-purple-200", - iconColor: "text-purple-500", - }, - orange: { - cardBg: "bg-orange-50", - iconBg: "bg-orange-100", - borderColor: "border-orange-200", - iconColor: "text-orange-500", - }, - red: { - cardBg: "bg-red-50", - iconBg: "bg-red-100", - borderColor: "border-red-200", - iconColor: "text-red-500", - }, -}; - export function FeatureCard({ icon: Icon, title, description, - colorScheme, + colorKey, }: FeatureCardProps) { - const colors = colorClasses[colorScheme]; + const colors = cardColorSchemes[colorKey]; return ( diff --git a/apps/client/src/pages/home/cards/JoinRoomCard.tsx b/apps/client/src/pages/home/cards/JoinRoomCard.tsx deleted file mode 100644 index 347bf33..0000000 --- a/apps/client/src/pages/home/cards/JoinRoomCard.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Hash } from "lucide-react"; - -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/shared/ui/card"; - -export function JoinRoomCard() { - return ( - - -
- -
- 방 번호로 입장 - - 기존 방 번호를 입력하여 협업에 참여하세요 - -
- -
- ); -} diff --git a/apps/client/src/pages/home/constants/card-color-schemes.ts b/apps/client/src/pages/home/constants/card-color-schemes.ts new file mode 100644 index 0000000..e3dc070 --- /dev/null +++ b/apps/client/src/pages/home/constants/card-color-schemes.ts @@ -0,0 +1,61 @@ +export interface CardColorScheme { + cardBg: string; + iconBg: string; + borderColor: string; + iconColor: string; + hoverCardBg: string; + hoverBorderColor: string; +} + +const blue: CardColorScheme = { + cardBg: "bg-blue-50", + iconBg: "bg-blue-100", + borderColor: "border-blue-200", + iconColor: "text-blue-500", + hoverCardBg: "hover:bg-blue-50", + hoverBorderColor: "hover:border-blue-500", +}; + +const green: CardColorScheme = { + cardBg: "bg-green-50", + iconBg: "bg-green-100", + borderColor: "border-green-200", + iconColor: "text-green-500", + hoverCardBg: "hover:bg-green-50", + hoverBorderColor: "hover:border-green-500", +}; + +const purple: CardColorScheme = { + cardBg: "bg-purple-50", + iconBg: "bg-purple-100", + borderColor: "border-purple-200", + iconColor: "text-purple-500", + hoverCardBg: "hover:bg-purple-50", + hoverBorderColor: "hover:border-purple-500", +}; + +const orange: CardColorScheme = { + cardBg: "bg-orange-50", + iconBg: "bg-orange-100", + borderColor: "border-orange-200", + iconColor: "text-orange-500", + hoverCardBg: "hover:bg-orange-50", + hoverBorderColor: "hover:border-orange-500", +}; + +const red: CardColorScheme = { + cardBg: "bg-red-50", + iconBg: "bg-red-100", + borderColor: "border-red-200", + iconColor: "text-red-500", + hoverCardBg: "hover:bg-red-50", + hoverBorderColor: "hover:border-red-500", +}; + +export const cardColorSchemes: Record = { + blue, + green, + purple, + orange, + red, +} as const; From e4db99c3d9830bb30c5234afa8e4134934cc5c13 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Thu, 8 Jan 2026 05:41:52 +0900 Subject: [PATCH 3/8] =?UTF-8?q?=E2=9C=A8feat:=20=EB=B0=A9=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=EB=B0=A9=20=EB=B2=88=ED=98=B8=EB=A1=9C?= =?UTF-8?q?=20=EC=9E=85=EC=9E=A5=EC=9D=84=20=EC=9C=84=ED=95=9C=20Dialog=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/pages/home/HomePage.tsx | 14 ++ .../pages/home/dialogs/CreateRoomDialog.tsx | 61 +++++++ .../src/pages/home/dialogs/JoinRoomDialog.tsx | 160 ++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx create mode 100644 apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx diff --git a/apps/client/src/pages/home/HomePage.tsx b/apps/client/src/pages/home/HomePage.tsx index 7ef0bec..1691548 100644 --- a/apps/client/src/pages/home/HomePage.tsx +++ b/apps/client/src/pages/home/HomePage.tsx @@ -1,9 +1,15 @@ +import { useState } from "react"; import { Users, Hash, CodeXml, Zap, UserCog } from "lucide-react"; import { Hero } from "./sections/Hero"; import { ActionCard } from "./cards/ActionCard"; import { FeatureCard } from "./cards/FeatureCard"; +import { CreateRoomDialog } from "./dialogs/CreateRoomDialog"; +import { JoinRoomDialog } from "./dialogs/JoinRoomDialog"; export default function MainPage() { + const [isCreateRoomOpen, setIsCreateRoomOpen] = useState(false); + const [isJoinRoomOpen, setIsJoinRoomOpen] = useState(false); + return (
@@ -15,12 +21,14 @@ export default function MainPage() { title="방 만들기" description="새로운 협업 공간을 생성하고 팀원들을 초대하세요" colorKey="blue" + onClick={() => setIsCreateRoomOpen(true)} /> setIsJoinRoomOpen(true)} />
@@ -45,6 +53,12 @@ export default function MainPage() { />
+ + +

); } diff --git a/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx b/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx new file mode 100644 index 0000000..c8b2391 --- /dev/null +++ b/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx @@ -0,0 +1,61 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { Button } from "@/shared/ui/button"; + +interface CreateRoomDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function CreateRoomDialog({ + open, + onOpenChange, +}: CreateRoomDialogProps) { + const handleQuickStart = () => { + console.log("Quick Start clicked"); + // Add your room creation logic here + }; + + const handleCustom = () => { + console.log("Custom clicked"); + // Add your custom room creation logic here + }; + + return ( + + + + 방 만들기 + + +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx b/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx new file mode 100644 index 0000000..7beed09 --- /dev/null +++ b/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx @@ -0,0 +1,160 @@ +import { useState, useRef } from "react"; +import type { KeyboardEvent, ClipboardEvent } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { Button } from "@/shared/ui/button"; + +const ROOM_CODE_LENGTH = 6; + +interface JoinRoomDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function JoinRoomDialog({ open, onOpenChange }: JoinRoomDialogProps) { + const [code, setCode] = useState(Array(ROOM_CODE_LENGTH).fill("")); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (index: number, value: string) => { + // Take the last character if multiple characters are present (happens when overwriting) + const newChar = value.slice(-1); + + if (newChar !== "" && !isValidRoomCodeChar(newChar)) return; + + const newCode = [...code]; + newCode[index] = newChar; + setCode(newCode); + + // Always move to next field after typing + if (newChar !== "" && index < ROOM_CODE_LENGTH - 1) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = (index: number, e: KeyboardEvent) => { + const value = (e.target as HTMLInputElement).value; + + // Backspace 처리 + // 현재 칸이 비어있으면 이전 칸으로 이동하고 값 지우기 + if (e.key === "Backspace") { + if (value === "" && index > 0) { + e.preventDefault(); + const newCode = [...code]; + newCode[index - 1] = ""; + setCode(newCode); + inputRefs.current[index - 1]?.focus(); + } + } + + // 왼쪽 화살표 + if (e.key === "ArrowLeft" && index > 0) { + e.preventDefault(); + inputRefs.current[index - 1]?.focus(); + } + + // 오른쪽 화살표 + if (e.key === "ArrowRight" && index < ROOM_CODE_LENGTH - 1) { + e.preventDefault(); + inputRefs.current[index + 1]?.focus(); + } + + // Enter 키로 제출 + if (e.key === "Enter") { + const roomCode = code.join(""); + if (roomCode.length === ROOM_CODE_LENGTH) { + handleSubmit(); + } + } + }; + + // 붙여넣기 처리 + const handlePaste = (index: number, e: ClipboardEvent) => { + e.preventDefault(); + + const pastedData = e.clipboardData + .getData("text") + .toUpperCase() + .slice(0, ROOM_CODE_LENGTH); + + // 영문 + 숫자만 필터링 + const validChars = pastedData + .split("") + .filter((char) => isValidRoomCodeChar(char)); + + if (validChars.length === 0) return; + + const newCode = [...code]; + + // 현재 index부터 채우기 + validChars.forEach((char, i) => { + const targetIndex = index + i; + if (targetIndex < ROOM_CODE_LENGTH) { + newCode[targetIndex] = char; + } + }); + + setCode(newCode); + + // 마지막으로 채워진 칸 다음으로 포커스 + const lastFilledIndex = Math.min( + index + validChars.length, + ROOM_CODE_LENGTH - 1 + ); + inputRefs.current[lastFilledIndex]?.focus(); + }; + + const handleSubmit = () => { + const roomCode = code.join(""); + if (roomCode.length === ROOM_CODE_LENGTH) { + console.log("Room code:", roomCode); + // Add your room join logic here + } + }; + + return ( + + + + 코드 입력 + + +
+
+ {code.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={(e) => handlePaste(index, e)} + className="w-12 h-12 text-center text-2xl font-semibold font-mono border-2 border-gray-300 focus:border-gray-700 focus:outline-none transition-colors uppercase caret-transparent" + /> + ))} +
+ + +
+
+
+ ); +} + +const isValidRoomCodeChar = (char: string): boolean => { + return /^[a-zA-Z0-9]$/.test(char); +}; From bea6926fc9535b8e549c094f64e27ad4aedb4c35 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Thu, 8 Jan 2026 20:08:58 +0900 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8feat:=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EB=88=8C=EB=9F=AC=EC=84=9C=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=EC=9D=84=20=EB=9D=84=EC=9A=B0=EB=8A=94=20=EA=B2=83=EC=9D=B4=20?= =?UTF-8?q?=EC=95=84=EB=8B=8C=20=EC=B9=B4=EB=93=9C=20=EC=95=88=EC=97=90=20?= =?UTF-8?q?=EB=82=B4=EC=9A=A9=EC=9D=84=20=EB=84=A3=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드를 누르지 않는 플로우로 수정 - 카드 안에 내용을 직접 삽입 --- apps/client/src/pages/home/HomePage.tsx | 59 +------ .../src/pages/home/cards/ActionCard.tsx | 15 +- .../pages/home/components/RoomCodeInput.tsx | 132 +++++++++++++++ .../pages/home/dialogs/CreateRoomDialog.tsx | 61 ------- .../src/pages/home/dialogs/JoinRoomDialog.tsx | 160 ------------------ .../src/pages/home/sections/ActionCards.tsx | 78 +++++++++ .../src/pages/home/sections/FeatureCards.tsx | 27 +++ apps/client/src/pages/home/sections/Hero.tsx | 2 +- 8 files changed, 250 insertions(+), 284 deletions(-) create mode 100644 apps/client/src/pages/home/components/RoomCodeInput.tsx delete mode 100644 apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx delete mode 100644 apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx create mode 100644 apps/client/src/pages/home/sections/ActionCards.tsx create mode 100644 apps/client/src/pages/home/sections/FeatureCards.tsx diff --git a/apps/client/src/pages/home/HomePage.tsx b/apps/client/src/pages/home/HomePage.tsx index 1691548..f42f81e 100644 --- a/apps/client/src/pages/home/HomePage.tsx +++ b/apps/client/src/pages/home/HomePage.tsx @@ -1,64 +1,15 @@ -import { useState } from "react"; -import { Users, Hash, CodeXml, Zap, UserCog } from "lucide-react"; import { Hero } from "./sections/Hero"; -import { ActionCard } from "./cards/ActionCard"; -import { FeatureCard } from "./cards/FeatureCard"; -import { CreateRoomDialog } from "./dialogs/CreateRoomDialog"; -import { JoinRoomDialog } from "./dialogs/JoinRoomDialog"; +import { ActionCards } from "./sections/ActionCards"; +import { FeatureCards } from "./sections/FeatureCards"; export default function MainPage() { - const [isCreateRoomOpen, setIsCreateRoomOpen] = useState(false); - const [isJoinRoomOpen, setIsJoinRoomOpen] = useState(false); - return (
-
+
- -
- setIsCreateRoomOpen(true)} - /> - setIsJoinRoomOpen(true)} - /> -
- -
- - - -
+ +
- - -
); } diff --git a/apps/client/src/pages/home/cards/ActionCard.tsx b/apps/client/src/pages/home/cards/ActionCard.tsx index 6321e71..dd036a6 100644 --- a/apps/client/src/pages/home/cards/ActionCard.tsx +++ b/apps/client/src/pages/home/cards/ActionCard.tsx @@ -13,7 +13,7 @@ interface ActionCardProps { title: string; description: string; colorKey: string; - onClick?: () => void; + children?: React.ReactNode; } export function ActionCard({ @@ -21,16 +21,13 @@ export function ActionCard({ title, description, colorKey, - onClick, + children, }: ActionCardProps) { const colors = cardColorSchemes[colorKey]; return ( - - + +
@@ -41,7 +38,9 @@ export function ActionCard({ {description} - + + {children} + ); } diff --git a/apps/client/src/pages/home/components/RoomCodeInput.tsx b/apps/client/src/pages/home/components/RoomCodeInput.tsx new file mode 100644 index 0000000..ac159fa --- /dev/null +++ b/apps/client/src/pages/home/components/RoomCodeInput.tsx @@ -0,0 +1,132 @@ +import { useRef } from "react"; +import type { KeyboardEvent, ClipboardEvent } from "react"; + +export const ROOM_CODE_LENGTH = 6; + +interface RoomCodeInputProps { + value: string[]; + onChange: (code: string[]) => void; + hasError?: boolean; + onSubmit?: () => void; + length?: number; +} + +export function RoomCodeInput({ + value, + onChange, + hasError = false, + onSubmit, + length = ROOM_CODE_LENGTH, +}: RoomCodeInputProps) { + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (index: number, inputValue: string) => { + if (inputValue !== "" && !isValidRoomCodeChar(inputValue)) return; + + const newCode = [...value]; + newCode[index] = inputValue; + onChange(newCode); + + // Move to next field after typing a character + if (inputValue !== "" && index < length - 1) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = (index: number, e: KeyboardEvent) => { + const inputValue = (e.target as HTMLInputElement).value; + + // Backspace handling + // If current field is empty, move to previous field and clear it + if (e.key === "Backspace") { + if (inputValue === "" && index > 0) { + e.preventDefault(); + const newCode = [...value]; + newCode[index - 1] = ""; + onChange(newCode); + inputRefs.current[index - 1]?.focus(); + } + } + + // Left arrow + if (e.key === "ArrowLeft" && index > 0) { + e.preventDefault(); + inputRefs.current[index - 1]?.focus(); + } + + // Right arrow + if (e.key === "ArrowRight" && index < length - 1) { + e.preventDefault(); + inputRefs.current[index + 1]?.focus(); + } + + // Enter key to submit + if (e.key === "Enter") { + const roomCode = value.join(""); + if (roomCode.length === length && onSubmit) { + onSubmit(); + } + } + }; + + // Paste handling + const handlePaste = (index: number, e: ClipboardEvent) => { + e.preventDefault(); + + const pastedData = e.clipboardData + .getData("text") + .toUpperCase() + .slice(0, length); + + // Filter only alphanumeric characters + const validChars = pastedData + .split("") + .filter((char) => isValidRoomCodeChar(char)); + + if (validChars.length === 0) return; + + const newCode = [...value]; + + // Fill from current index + validChars.forEach((char, i) => { + const targetIndex = index + i; + if (targetIndex < length) { + newCode[targetIndex] = char; + } + }); + + onChange(newCode); + + // Set new focus + const nextIndex = Math.min(index + validChars.length, length - 1); + inputRefs.current[nextIndex]?.focus(); + }; + + return ( +
+ {value.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={(e) => handlePaste(index, e)} + className={`w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-center text-base sm:text-xl md:text-2xl font-semibold font-mono border-2 ${ + hasError + ? "border-red-500" + : "border-gray-300 focus:border-gray-700" + } focus:outline-none transition-colors uppercase caret-transparent`} + /> + ))} +
+ ); +} + +const isValidRoomCodeChar = (char: string): boolean => { + return /^[a-zA-Z0-9]$/.test(char); +}; diff --git a/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx b/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx deleted file mode 100644 index c8b2391..0000000 --- a/apps/client/src/pages/home/dialogs/CreateRoomDialog.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/shared/ui/dialog"; -import { Button } from "@/shared/ui/button"; - -interface CreateRoomDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function CreateRoomDialog({ - open, - onOpenChange, -}: CreateRoomDialogProps) { - const handleQuickStart = () => { - console.log("Quick Start clicked"); - // Add your room creation logic here - }; - - const handleCustom = () => { - console.log("Custom clicked"); - // Add your custom room creation logic here - }; - - return ( - - - - 방 만들기 - - -
-
- -
-
- -
-
-
-
- ); -} diff --git a/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx b/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx deleted file mode 100644 index 7beed09..0000000 --- a/apps/client/src/pages/home/dialogs/JoinRoomDialog.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useState, useRef } from "react"; -import type { KeyboardEvent, ClipboardEvent } from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/shared/ui/dialog"; -import { Button } from "@/shared/ui/button"; - -const ROOM_CODE_LENGTH = 6; - -interface JoinRoomDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function JoinRoomDialog({ open, onOpenChange }: JoinRoomDialogProps) { - const [code, setCode] = useState(Array(ROOM_CODE_LENGTH).fill("")); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - const handleChange = (index: number, value: string) => { - // Take the last character if multiple characters are present (happens when overwriting) - const newChar = value.slice(-1); - - if (newChar !== "" && !isValidRoomCodeChar(newChar)) return; - - const newCode = [...code]; - newCode[index] = newChar; - setCode(newCode); - - // Always move to next field after typing - if (newChar !== "" && index < ROOM_CODE_LENGTH - 1) { - inputRefs.current[index + 1]?.focus(); - } - }; - - const handleKeyDown = (index: number, e: KeyboardEvent) => { - const value = (e.target as HTMLInputElement).value; - - // Backspace 처리 - // 현재 칸이 비어있으면 이전 칸으로 이동하고 값 지우기 - if (e.key === "Backspace") { - if (value === "" && index > 0) { - e.preventDefault(); - const newCode = [...code]; - newCode[index - 1] = ""; - setCode(newCode); - inputRefs.current[index - 1]?.focus(); - } - } - - // 왼쪽 화살표 - if (e.key === "ArrowLeft" && index > 0) { - e.preventDefault(); - inputRefs.current[index - 1]?.focus(); - } - - // 오른쪽 화살표 - if (e.key === "ArrowRight" && index < ROOM_CODE_LENGTH - 1) { - e.preventDefault(); - inputRefs.current[index + 1]?.focus(); - } - - // Enter 키로 제출 - if (e.key === "Enter") { - const roomCode = code.join(""); - if (roomCode.length === ROOM_CODE_LENGTH) { - handleSubmit(); - } - } - }; - - // 붙여넣기 처리 - const handlePaste = (index: number, e: ClipboardEvent) => { - e.preventDefault(); - - const pastedData = e.clipboardData - .getData("text") - .toUpperCase() - .slice(0, ROOM_CODE_LENGTH); - - // 영문 + 숫자만 필터링 - const validChars = pastedData - .split("") - .filter((char) => isValidRoomCodeChar(char)); - - if (validChars.length === 0) return; - - const newCode = [...code]; - - // 현재 index부터 채우기 - validChars.forEach((char, i) => { - const targetIndex = index + i; - if (targetIndex < ROOM_CODE_LENGTH) { - newCode[targetIndex] = char; - } - }); - - setCode(newCode); - - // 마지막으로 채워진 칸 다음으로 포커스 - const lastFilledIndex = Math.min( - index + validChars.length, - ROOM_CODE_LENGTH - 1 - ); - inputRefs.current[lastFilledIndex]?.focus(); - }; - - const handleSubmit = () => { - const roomCode = code.join(""); - if (roomCode.length === ROOM_CODE_LENGTH) { - console.log("Room code:", roomCode); - // Add your room join logic here - } - }; - - return ( - - - - 코드 입력 - - -
-
- {code.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={(e) => handlePaste(index, e)} - className="w-12 h-12 text-center text-2xl font-semibold font-mono border-2 border-gray-300 focus:border-gray-700 focus:outline-none transition-colors uppercase caret-transparent" - /> - ))} -
- - -
-
-
- ); -} - -const isValidRoomCodeChar = (char: string): boolean => { - return /^[a-zA-Z0-9]$/.test(char); -}; diff --git a/apps/client/src/pages/home/sections/ActionCards.tsx b/apps/client/src/pages/home/sections/ActionCards.tsx new file mode 100644 index 0000000..2dbbd85 --- /dev/null +++ b/apps/client/src/pages/home/sections/ActionCards.tsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { Users, Hash } from "lucide-react"; +import { ActionCard } from "../cards/ActionCard"; +import { RoomCodeInput, ROOM_CODE_LENGTH } from "../components/RoomCodeInput"; +import { Button } from "@/shared/ui/button"; + +interface ErrorMessageProps { + message: string; +} + +function ErrorMessage({ message }: ErrorMessageProps) { + return ( +
+ {message &&

{message}

} +
+ ); +} + +export function ActionCards() { + const [roomCode, setRoomCode] = useState( + Array(ROOM_CODE_LENGTH).fill("") + ); + const [errorMessage, setErrorMessage] = useState(""); + + const handleQuickStart = () => { + console.log("Quick Start clicked"); + // Add your room creation logic here + }; + + const handleJoinRoom = () => { + const code = roomCode.join(""); + if (code.length !== ROOM_CODE_LENGTH) return; + + // Add your room join logic here + }; + + return ( +
+ + + + + +
+ + + +
+
+
+ ); +} diff --git a/apps/client/src/pages/home/sections/FeatureCards.tsx b/apps/client/src/pages/home/sections/FeatureCards.tsx new file mode 100644 index 0000000..d0c902e --- /dev/null +++ b/apps/client/src/pages/home/sections/FeatureCards.tsx @@ -0,0 +1,27 @@ +import { CodeXml, Zap, UserCog } from "lucide-react"; +import { FeatureCard } from "../cards/FeatureCard"; + +export function FeatureCards() { + return ( +
+ + + +
+ ); +} diff --git a/apps/client/src/pages/home/sections/Hero.tsx b/apps/client/src/pages/home/sections/Hero.tsx index 4c8f5f6..31248b5 100644 --- a/apps/client/src/pages/home/sections/Hero.tsx +++ b/apps/client/src/pages/home/sections/Hero.tsx @@ -2,7 +2,7 @@ import logoAnimation from "@/assets/logo_animation.svg"; export function Hero() { return ( -
+
CodeJam Logo
From 8af60e0223670ab8959bfcf651b14a48d0170479 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Thu, 8 Jan 2026 21:46:17 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=A8feat:=20ActionCard=20=EC=99=80=20F?= =?UTF-8?q?eatureCard=20Section=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 각 카드의 UI 가 직관적으로 분리되도록 함 --- .../src/pages/home/cards/ActionCard.tsx | 11 +-- .../src/pages/home/cards/FeatureCard.tsx | 13 ++-- .../pages/home/components/RoomCodeInput.tsx | 2 +- .../home/constants/card-color-schemes.ts | 10 +-- .../src/pages/home/sections/ActionCards.tsx | 72 ++++++++++--------- .../src/pages/home/sections/FeatureCards.tsx | 46 ++++++------ 6 files changed, 82 insertions(+), 72 deletions(-) diff --git a/apps/client/src/pages/home/cards/ActionCard.tsx b/apps/client/src/pages/home/cards/ActionCard.tsx index dd036a6..a2fab09 100644 --- a/apps/client/src/pages/home/cards/ActionCard.tsx +++ b/apps/client/src/pages/home/cards/ActionCard.tsx @@ -26,19 +26,22 @@ export function ActionCard({ const colors = cardColorSchemes[colorKey]; return ( - - + +
+ {title} {description}
- + {children}
diff --git a/apps/client/src/pages/home/cards/FeatureCard.tsx b/apps/client/src/pages/home/cards/FeatureCard.tsx index c345898..6946459 100644 --- a/apps/client/src/pages/home/cards/FeatureCard.tsx +++ b/apps/client/src/pages/home/cards/FeatureCard.tsx @@ -19,14 +19,13 @@ export function FeatureCard({ return ( - -
-
- -
+ +
+
+ {title} {description} diff --git a/apps/client/src/pages/home/components/RoomCodeInput.tsx b/apps/client/src/pages/home/components/RoomCodeInput.tsx index ac159fa..affc0c6 100644 --- a/apps/client/src/pages/home/components/RoomCodeInput.tsx +++ b/apps/client/src/pages/home/components/RoomCodeInput.tsx @@ -119,7 +119,7 @@ export function RoomCodeInput({ className={`w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-center text-base sm:text-xl md:text-2xl font-semibold font-mono border-2 ${ hasError ? "border-red-500" - : "border-gray-300 focus:border-gray-700" + : "border-gray-400 focus:border-gray-900" } focus:outline-none transition-colors uppercase caret-transparent`} /> ))} diff --git a/apps/client/src/pages/home/constants/card-color-schemes.ts b/apps/client/src/pages/home/constants/card-color-schemes.ts index e3dc070..d72d61c 100644 --- a/apps/client/src/pages/home/constants/card-color-schemes.ts +++ b/apps/client/src/pages/home/constants/card-color-schemes.ts @@ -13,7 +13,7 @@ const blue: CardColorScheme = { borderColor: "border-blue-200", iconColor: "text-blue-500", hoverCardBg: "hover:bg-blue-50", - hoverBorderColor: "hover:border-blue-500", + hoverBorderColor: "hover:border-blue-400", }; const green: CardColorScheme = { @@ -22,7 +22,7 @@ const green: CardColorScheme = { borderColor: "border-green-200", iconColor: "text-green-500", hoverCardBg: "hover:bg-green-50", - hoverBorderColor: "hover:border-green-500", + hoverBorderColor: "hover:border-green-400", }; const purple: CardColorScheme = { @@ -31,7 +31,7 @@ const purple: CardColorScheme = { borderColor: "border-purple-200", iconColor: "text-purple-500", hoverCardBg: "hover:bg-purple-50", - hoverBorderColor: "hover:border-purple-500", + hoverBorderColor: "hover:border-purple-400", }; const orange: CardColorScheme = { @@ -40,7 +40,7 @@ const orange: CardColorScheme = { borderColor: "border-orange-200", iconColor: "text-orange-500", hoverCardBg: "hover:bg-orange-50", - hoverBorderColor: "hover:border-orange-500", + hoverBorderColor: "hover:border-orange-400", }; const red: CardColorScheme = { @@ -49,7 +49,7 @@ const red: CardColorScheme = { borderColor: "border-red-200", iconColor: "text-red-500", hoverCardBg: "hover:bg-red-50", - hoverBorderColor: "hover:border-red-500", + hoverBorderColor: "hover:border-red-400", }; export const cardColorSchemes: Record = { diff --git a/apps/client/src/pages/home/sections/ActionCards.tsx b/apps/client/src/pages/home/sections/ActionCards.tsx index 2dbbd85..8f60f69 100644 --- a/apps/client/src/pages/home/sections/ActionCards.tsx +++ b/apps/client/src/pages/home/sections/ActionCards.tsx @@ -35,44 +35,46 @@ export function ActionCards() { }; return ( -
- - - - - -
- - -
-
-
+ + + +
+ + + +
+
+
+ ); } diff --git a/apps/client/src/pages/home/sections/FeatureCards.tsx b/apps/client/src/pages/home/sections/FeatureCards.tsx index d0c902e..956bbb6 100644 --- a/apps/client/src/pages/home/sections/FeatureCards.tsx +++ b/apps/client/src/pages/home/sections/FeatureCards.tsx @@ -3,25 +3,31 @@ import { FeatureCard } from "../cards/FeatureCard"; export function FeatureCards() { return ( -
- - - -
+
+

+ Features +

+ +
+ + + +
+
); } From 8823718770c9a45b5086098ee96dd2b2f17bdd63 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Fri, 9 Jan 2026 00:13:49 +0900 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9C=A8feat:=20Create=20Quick=20Start=20R?= =?UTF-8?q?oom,=20Join=20Room=20API=20=EC=97=B0=EA=B2=B0=20#68?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/home/components/RoomCodeInput.tsx | 6 +- .../src/pages/home/sections/ActionCards.tsx | 77 ++++++++++++++----- apps/client/src/shared/api/room.ts | 54 +++++++++++++ .../src/modules/room/room.controller.ts | 15 +++- apps/server/src/modules/room/room.service.ts | 5 +- 5 files changed, 130 insertions(+), 27 deletions(-) create mode 100644 apps/client/src/shared/api/room.ts diff --git a/apps/client/src/pages/home/components/RoomCodeInput.tsx b/apps/client/src/pages/home/components/RoomCodeInput.tsx index affc0c6..9d4349e 100644 --- a/apps/client/src/pages/home/components/RoomCodeInput.tsx +++ b/apps/client/src/pages/home/components/RoomCodeInput.tsx @@ -117,10 +117,8 @@ export function RoomCodeInput({ onKeyDown={(e) => handleKeyDown(index, e)} onPaste={(e) => handlePaste(index, e)} className={`w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 text-center text-base sm:text-xl md:text-2xl font-semibold font-mono border-2 ${ - hasError - ? "border-red-500" - : "border-gray-400 focus:border-gray-900" - } focus:outline-none transition-colors uppercase caret-transparent`} + hasError ? "border-red-500" : "border-gray-400" + } focus:border-gray-900 focus:outline-none transition-colors uppercase caret-transparent`} /> ))}
diff --git a/apps/client/src/pages/home/sections/ActionCards.tsx b/apps/client/src/pages/home/sections/ActionCards.tsx index 8f60f69..7b5918d 100644 --- a/apps/client/src/pages/home/sections/ActionCards.tsx +++ b/apps/client/src/pages/home/sections/ActionCards.tsx @@ -3,6 +3,7 @@ import { Users, Hash } from "lucide-react"; import { ActionCard } from "../cards/ActionCard"; import { RoomCodeInput, ROOM_CODE_LENGTH } from "../components/RoomCodeInput"; import { Button } from "@/shared/ui/button"; +import { createQuickRoom, joinRoom } from "@/shared/api/room"; interface ErrorMessageProps { message: string; @@ -10,8 +11,12 @@ interface ErrorMessageProps { function ErrorMessage({ message }: ErrorMessageProps) { return ( -
- {message &&

{message}

} +
+ {message && ( +

+ {message} +

+ )}
); } @@ -20,18 +25,50 @@ export function ActionCards() { const [roomCode, setRoomCode] = useState( Array(ROOM_CODE_LENGTH).fill("") ); - const [errorMessage, setErrorMessage] = useState(""); + const [quickStartError, setQuickStartError] = useState(""); + const [joinRoomError, setJoinRoomError] = useState(""); + const [isQuickStartLoading, setIsQuickStartLoading] = useState(false); + const [isJoinRoomLoading, setIsJoinRoomLoading] = useState(false); - const handleQuickStart = () => { - console.log("Quick Start clicked"); - // Add your room creation logic here + const handleQuickStart = async () => { + if (isQuickStartLoading) return; + + setIsQuickStartLoading(true); + setQuickStartError(""); + + try { + const { roomCode, myPtId } = await createQuickRoom(); + + // Save PT ID first + const key = `ptId:${roomCode}`; + localStorage.setItem(key, myPtId); + + // Then join the room + await joinRoom(roomCode); + } catch (e) { + const error = e as Error; + setQuickStartError(error.message); + } finally { + setIsQuickStartLoading(false); + } }; - const handleJoinRoom = () => { + const handleJoinRoom = async () => { const code = roomCode.join(""); if (code.length !== ROOM_CODE_LENGTH) return; + if (isJoinRoomLoading) return; + + setIsJoinRoomLoading(true); + setJoinRoomError(""); - // Add your room join logic here + try { + await joinRoom(code); + } catch (e) { + const error = e as Error; + setJoinRoomError(error.message); + } finally { + setIsJoinRoomLoading(false); + } }; return ( @@ -43,12 +80,16 @@ export function ActionCards() { description="새로운 협업 공간을 생성하고 팀원들을 초대하세요" colorKey="blue" > - +
+ + +
- +
diff --git a/apps/client/src/shared/api/room.ts b/apps/client/src/shared/api/room.ts new file mode 100644 index 0000000..4be54aa --- /dev/null +++ b/apps/client/src/shared/api/room.ts @@ -0,0 +1,54 @@ +// Room REST API +// TODO: Error message mapping + +const ROOM_API_PREFIX = "/api/rooms"; + +interface CreateQuickRoomResponse { + roomCode: string; + myPtId: string; +} + +export async function checkRoomExists(roomCode: string): Promise { + try { + const response = await fetch(`${ROOM_API_PREFIX}/${roomCode}/exists`); + const { exists } = await response.json(); + return exists; + } catch (e) { + const error = e as Error; + throw error; + } +} + +export async function createQuickRoom(): Promise { + try { + const response = await fetch(`${ROOM_API_PREFIX}/quick`, { + method: "POST", + }); + + if (!response.ok) { + const message = "Failed to create quick room"; + throw new Error(message); + } + + return await response.json(); + } catch (e) { + const error = e as Error; + throw error; + } +} + +export async function joinRoom(roomCode: string): Promise { + try { + const response = await fetch(`${ROOM_API_PREFIX}/${roomCode}/join`, { + method: "POST", + }); + + if (!response.ok) { + const message = "Failed to join room"; + throw new Error(message); + } + } catch (e) { + const error = e as Error; + throw error; + } +} diff --git a/apps/server/src/modules/room/room.controller.ts b/apps/server/src/modules/room/room.controller.ts index 4854dbf..f7a56d7 100644 --- a/apps/server/src/modules/room/room.controller.ts +++ b/apps/server/src/modules/room/room.controller.ts @@ -2,13 +2,14 @@ import { Controller, Get, Param, - NotFoundException, Post, + Redirect, + NotFoundException, } from '@nestjs/common'; import { RoomService } from './room.service'; import { CreateRoomResponseDto } from './dto/create-room-response.dto'; -@Controller('api/room') +@Controller('api/rooms') export class RoomController { constructor(private readonly roomService: RoomService) {} @@ -21,6 +22,16 @@ export class RoomController { return { exists: true }; } + @Get(':roomCode/join') + @Redirect() + async redirectToRoom(@Param('roomCode') roomCode: string) { + const exists = await this.roomService.roomExists(roomCode); + if (!exists) throw new NotFoundException(); + + const redirectUrl = `/rooms/${roomCode}`; + return { url: redirectUrl }; + } + @Post('quick') async createQuickRoom(): Promise { return await this.roomService.createQuickRoom(); diff --git a/apps/server/src/modules/room/room.service.ts b/apps/server/src/modules/room/room.service.ts index 7dd3f21..6f764a4 100644 --- a/apps/server/src/modules/room/room.service.ts +++ b/apps/server/src/modules/room/room.service.ts @@ -114,8 +114,7 @@ export class RoomService { } protected generateRoomCode(roomCodeLength = 6): string { - const alphabet = - '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const nanoid = customAlphabet(alphabet, roomCodeLength); return nanoid(); } @@ -125,7 +124,7 @@ export class RoomService { */ async findRoomIdByCode(roomCode: string): Promise { const room = await this.roomRepository.findOne({ - where: { code: roomCode }, + where: { roomCode }, select: ['roomId'], }); return room?.roomId ?? null; From 75ddd2c332a369e438ec4f8948603f840b633512 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Fri, 9 Jan 2026 00:15:57 +0900 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=93=9Ddocs:=20=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20layout=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/layout.md | 766 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 766 insertions(+) create mode 100644 docs/layout.md diff --git a/docs/layout.md b/docs/layout.md new file mode 100644 index 0000000..630d608 --- /dev/null +++ b/docs/layout.md @@ -0,0 +1,766 @@ +## Layout Variations + +### Horizontal Split Layout + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | +|---------------------------------------------------------| +| | +| +------------------------+ +---------------------+ | +| | | | | | +| | [ CREATE ROOM ] | | [ Join Room ] | | +| | | | | | +| | +----------------+ | | Enter code | | +| | | ⚡︎︎︎ QUICK START | | | [X][Y][Z][1][2][3] | | +| | +----------------+ | | | | +| | | | [ Join → ] | | +| | > Advanced Settings | | | | +| | | | | | +| +------------------------+ +---------------------+ | +| | +|---------------------------------------------------------| +| +-------------+ +-------------+ +-------------+ | +| | ⚡︎︎︎ | | | | ☼︎ | | +| | ... | | ... | | ... | | +| +-------------+ +-------------+ +-------------+ | ++---------------------------------------------------------+ +``` + +### Vertical Layout + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | +|---------------------------------------------------------| +| | +| +-------------------------------------------------+ | +| | | | +| | [ CREATE ROOM ] | | +| | | | +| | +-----------------------------------------+ | | +| | | ⚡︎︎︎ QUICK START | | | +| | +-----------------------------------------+ | | +| | | | +| | > Advanced Settings | | +| | | | +| +-------------------------------------------------+ | +| | +| --- OR --- | +| | +| +-------------------------------------------------+ | +| | | | +| | [ Join Existing ROOM ] | | +| | (Enter your code to participate) | | +| | | | +| | [X] [Y] [Z] [1] [2] [3] | | +| | [ Join Room → ] | | +| | | | +| +-------------------------------------------------+ | +|---------------------------------------------------------| +| | +| +-------------+ +---------------+ +-------------+ | +| | ⚡︎︎︎ | | | | ☼︎ | | +| | ... | | ... | | ... | | +| +-------------+ +---------------+ +-------------+ | +| | ++---------------------------------------------------------+ +``` + +### Tab Layout + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | +|---------------------------------------------------------| +| | +| [ Create Room ] [ Join Room ] | +| ━━━━━━━━━━━━━━━ ───────────── | +| | +| +-------------------------------------------------+ | +| | | | +| | [ CREATE ROOM ] | | +| | | | +| | +-----------------------------------------+ | | +| | | ⚡︎︎︎ QUICK START | | | +| | +-----------------------------------------+ | | +| | | | +| | > Advanced Settings | | +| | | | +| +-------------------------------------------------+ | +| | +|---------------------------------------------------------| +| | +| +-------------+ +---------------+ +-------------+ | +| | ⚡︎︎︎ | | | | ☼︎ | | +| | ... | | ... | | ... | | +| +-------------+ +---------------+ +-------------+ | +| | ++---------------------------------------------------------+ + +``` + +### Card Grid Layout + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | +|---------------------------------------------------------| +| | +| +---------------------+ +---------------------+ | +| | | | | | +| | ⚡︎︎︎ Quick Start | | # Join Room | | +| | | | | | +| | Create instantly | | [X][Y][Z][1][2][3] | | +| | | | | | +| | [ Start → ] | | [ Join → ] | | +| | | | | | +| +---------------------+ +---------------------+ | +| | +| +---------------------+ | +| | | | +| | ☼︎ Custom Setup | | +| | | | +| | Advanced options | | +| | | | +| | [ Configure → ] | | +| | | | +| +---------------------+ | +| | +|---------------------------------------------------------| +| | +| +-------------+ +---------------+ +-------------+ | +| | ⚡︎︎︎ | | | | ☼︎ | | +| | ... | | ... | | ... | | +| +-------------+ +---------------+ +-------------+ | +| | ++---------------------------------------------------------+ +``` + +### Minimal Center Layout + +``` ++---------------------------------------------------------+ +| | +| [logo] | +| CodeJam | +| Real-time Collaborative Code Editor | +| | +| | +| +-------------------------------+ | +| | [ ⚡︎︎︎ Quick Start ] | | +| +-------------------------------+ | +| | +| +-------------------------------+ | +| | [X] [Y] [Z] [1] [2] [3] | | +| | [ Join Room → ] | | +| +-------------------------------+ | +| | +| > Advanced Settings | +| | +| | +|---------------------------------------------------------| +| ⚡︎︎︎ Real-time Multi-language ☼︎ Permissions | ++---------------------------------------------------------+ +``` + +### Compact Inline Layout + +``` ++---------------------------------------------------------+ +| CodeJam | +| Real-time Collaborative Code Editor | +|---------------------------------------------------------| +| | +| [ ⚡︎︎︎ Quick Start ] or [X][Y][Z][1][2][3] [ Join ] | +| | +| > Advanced room settings | +| | +|---------------------------------------------------------| +| | +| ⚡︎︎︎ Real-time • Multi-language • ☼︎ Access | +| | ++---------------------------------------------------------+ +``` + +--- + +## Feature Section Variations + +### Card Style + +``` ++---------------------------------------------------------+ +| +---------------+ +---------------+ +-------------+ | +| | ⚡︎︎︎ | | | | ☼︎ | | +| | ... | | ... | | ... | | +| +---------------+ +---------------+ +-------------+ | ++---------------------------------------------------------+ +``` + +### Icon Grid + +``` ++---------------------------------------------------------+ +| | +| ⚡︎︎︎ ☼︎ | +| | +| Real-time Multi-language Permissions | +| Sync Support Control | +| | ++---------------------------------------------------------+ +``` + +### Icon grid with dividers + +``` ++---------------------------------------------------------+ +| ⚡︎︎︎ │ │ ☼︎ | +| ... │ ... │ ... | ++---------------------------------------------------------+ +``` + +### Tab Style + +``` ++---------------------------------------------------------+ +| | +| [ Real-time Sync ] [ Multi-language ] [ Access ] | +| ━━━━━━━━━━━━━━━━━ | +| | +| ⚡︎︎︎ Real-time Synchronization | +| | +| Collaborate seamlessly with your team. See every | +| change instantly as your teammates type. No delays, | +| no conflicts, just pure productivity. | +| | +| • Zero-latency updates | +| • Automatic conflict resolution | +| • Live cursor tracking | +| | ++---------------------------------------------------------+ +``` + +### Vertical List + +``` ++---------------------------------------------------------+ +| ⚡︎︎︎ Real-time Sync | +| Multi-language Support | +| ☼︎ Permission Control | ++---------------------------------------------------------+ +``` + +### Accordion Style + +``` ++---------------------------------------------------------+ +| ▶ Real-time Sync | +| ⯆ Multi-language Support | +| Python, JavaScript, Go, Rust, TypeScript... | +| ▶ Permission Control | +| | ++---------------------------------------------------------+ +``` + +### Inline List + +``` ++--------------------------------------------------------------+ +| ⚡︎︎︎ Real-time Sync • Multi-language • ☼︎ Permission Control | ++--------------------------------------------------------------+ +``` + +### Icon + Text Inline + +``` ++-----------------------------------------------------------------------+ +| | +| ⚡︎︎︎ Real-time Sync Multi-language ☼︎ Permissions Control | +| Fast collaboration C, Python, JS, ... Host, Editor, Viewer | +| | ++-----------------------------------------------------------------------+ +``` + +### Timeline Style + +``` ++---------------------------------------------------------+ +| | +| ⚡︎︎︎ ───────────────────────────── | +| Real-time synchronization | +| Instant updates with no lag | +| | +| ──────────────────────────── | +| Multi-language support | +| 20+ languages with syntax highlighting | +| | +| ☼︎ ───────────────────────────── | +| Permission control | +| Granular role-based access | +| | ++---------------------------------------------------------+ +``` + +--- + +## Create Room Card Variations + +### Expandable Section + +``` ++-------------------------------------------------+ +| | +| [ CREATE ROOM ] | +| | +| +-----------------------------------------+ | +| | ⚡︎︎︎ QUICK START | | +| | Create a room instantly | | +| +-----------------------------------------+ | +| | +| > Advanced Settings | +| (Expand when clicked) | +| | ++-------------------------------------------------+ + +// When expanded: ++-------------------------------------------------+ +| | +| [ CREATE ROOM ] | +| | +| +-----------------------------------------+ | +| | ⚡︎︎︎ QUICK START | | +| +-----------------------------------------+ | +| | +| ∨ Advanced Settings | +| ┌─────────────────────────────────────────┐ | +| │ Text: [___________] │ | +| │ Radio: ( ) Public (•) Private │ | +| │ Dropdown: [JavaScript ▾] │ | +| │ [Create] │ | +| └─────────────────────────────────────────┘ | +| | ++-------------------------------------------------+ +``` + +### Secondary Button + Modal + +``` ++-------------------------------------------------+ +| | +| [ CREATE ROOM ] | +| | +| +-----------------------------------------+ | +| | ⚡︎︎︎ QUICK START | | +| | Create a room instantly | | +| +-----------------------------------------+ | +| | +| [ Advanced Settings... ] | +| (Click to open modal) | +| | ++-------------------------------------------------+ + +// When clicked, modal appears: ++---------------------------------------------------+ +| ╔════════════════════════════════════════════╗ | +| ║ ║ | +| ║ [ Advanced Room Settings ] [X] ║ | +| ║ ║ | +| ║ Text ║ | +| ║ [________________________________] ║ | +| ║ ║ | +| ║ Radio ║ | +| ║ ( ) Public (•) Private ║ | +| ║ ║ | +| ║ Dropdown ║ | +| ║ [ JavaScript ▾ ] ║ | +| ║ ║ | +| ║ Checkbox ║ | +| ║ [√] Auto-save ║ | +| ║ [√] Code execution ║ | +| ║ [ ] Chat ║ | +| ║ ║ | +| ║ [ Cancel ] [ Create → ] ║ | +| ║ ║ | +| ╚════════════════════════════════════════════╝ | ++---------------------------------------------------+ +``` + +### Side-by-Side Cards + +``` ++-----------------------------------------------------+ +| [ CREATE ROOM ] | +| | +| +--------------------+ +---------------------+ | +| | | | | | +| | ⚡︎︎ Quick Start | | ☼︎ Custom Setup | | +| | | | | | +| | Create instantly | | Advanced options | | +| | with defaults | | and configurations | | +| | | | | | +| | [ Start → ] | | [ Configure → ] | | +| | | | | | +| +--------------------+ +---------------------+ | +| | ++-----------------------------------------------------+ +``` + +### Stacked Cards + +``` ++-------------------------------------------------+ +| [ CREATE ROOM ] | +| | +| +----------------------------------------+ | +| | ⚡ Quick Start | | +| | | | +| | Create a room instantly with default | | +| | settings | | +| | | | +| | [ Start → ] | | +| +----------------------------------------+ | +| | +| +----------------------------------------+ | +| | ☼ Custom Setup | | +| | | | +| | Configure advanced room options and | | +| | settings | | +| | | | +| | [ Configure → ] | | +| +----------------------------------------+ | +| | ++-------------------------------------------------+ +``` + +### Tab Style + +``` ++-------------------------------------------------+ +| [ CREATE ROOM ] | +| | +| [ ⚡ Quick Start ] [ ☼ Custom Setup ] | +| ━━━━━━━━━━━━━━━━━ ────────────────── | +| | +| +----------------------------------------+ | +| | | | +| | Create a room instantly with default | | +| | settings. Perfect for quick | | +| | collaboration sessions. | | +| | | | +| | [ Start → ] | | +| | | | +| +----------------------------------------+ | +| | ++-------------------------------------------------+ +``` + +### Icon Grid + Progressive Disclosure + +#### Step 1: Initial Choice + +``` ++-------------------------------------------------+ +| [ CREATE ROOM ] | +| | +| | +| +---------------+ +---------------+ | +| | | | | | +| | ⚡ | | ☼ | | +| | | | | | +| | Quick Start | | Custom Setup | | +| | | | | | +| +---------------+ +---------------+ | +| | +| | ++-------------------------------------------------+ +``` + +#### Step 2A: Quick Start + +``` ++-------------------------------------------------+ +| [ CREATE ROOM ] | +| ⚡ Quick Start Selected | +| | +| Creating room with default settings... | +| | +| ████████████████░░░░░░░░░░ 60% | +| | +| → Redirecting to room... | +| | ++-------------------------------------------------+ +``` + +#### Step 2B: Custom Setup → Progressive Form + +``` ++-------------------------------------------------+ +| [ CREATE ROOM ] | +| ☼ Custom Setup Selected | +| | +| Text | +| [__________] | +| | +| Radio | +| ( ) Public (•) Private | +| | +| Dropdown | +| [ JavaScript ▾ ] | +| | +| Checkbox | +| [√] Auto-save | +| [√] Code execution | +| [ ] Chat | +| | +| [ Back ] [ Create ] | +| | ++-------------------------------------------------+ +``` + +--- + +## Room Code Input Field Variations + +### Brackets + +``` ++-------------------------------------------------+ +| [ X ] [ Y ] [ Z ] [ 1 ] [ 2 ] [ 3 ] | ++-------------------------------------------------+ +``` + +### Box (Monospace) + +``` ++-------------------------------------------------+ +| | +| ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ | +| │ X │ │ Y │ │ Z │ │ 1 │ │ 2 │ │ 3 │ | +| └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ | +| | ++-------------------------------------------------+ +``` + +### Double-lined Boxes + +``` ++-------------------------------------------------+ +| | +| ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ ╔═══╗ | +| ║ X ║ ║ Y ║ ║ Z ║ ║ 1 ║ ║ 2 ║ ║ 3 ║ | +| ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ ╚═══╝ | +| | ++-------------------------------------------------+ +``` + +### Underline Style (Minimal) + +``` ++-------------------------------------------------+ +| | +| X Y Z 1 2 3 | +| ─── ─── ─── ─── ─── ─── | +| | ++-------------------------------------------------+ +``` + +### Single Input with Wide Letter Spacing + +``` ++-------------------------------------------------+ +| | +| ┌───────────────────────────────────┐ | +| │ X Y Z 1 2 3 │ | +| └───────────────────────────────────┘ | +| (letter-spacing: 2em or wider) | +| | ++-------------------------------------------------+ + +// With cursor: ++-------------------------------------------------+ +| | +| ┌───────────────────────────────────┐ | +| │ X Y Z 1 2 3 | │ | +| └───────────────────────────────────┘ | +| | ++-------------------------------------------------+ + +// Empty state: ++-------------------------------------------------+ +| ┌───────────────────────────────────┐ | +| │ _ _ _ _ _ _ │ | +| └───────────────────────────────────┘ | ++-------------------------------------------------+ +``` + +--- + +## Focus Indicator Variations + +### Underline Blinking + +``` +Frame 1 (0.0s): +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │ _ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └─█─┘ └───┘ └───┘ └───┘ + ↑ + (blinking) + +Frame 2 (0.5s): +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │ _ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └─▁─┘ └───┘ └───┘ └───┘ + ↑ + (blinking) +``` + +### Vertical Cursor Blinking + +``` +Frame 1 (0.0s): +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │|_ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + ↑ + (currently typing) + +Frame 2 (0.5s): +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │ _ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + ↑ + (cursor hidden) +``` + +### Bold Border + +``` +┌───┐ ┌───┐ ╔═══╗ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ ║ _ ║ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ ╚═══╝ └───┘ └───┘ └───┘ + ↑ + (active focus) +``` + +### Color Border + +``` +┌───┐ ┌───┐ ┏━━━┓ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ ┃ _ ┃ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ ┗━━━┛ └───┘ └───┘ └───┘ + ↑ + (blue/green border) +``` + +### Shadow/Glow Effect + +``` + ▒▒▒▒▒▒▒ +┌───┐ ┌───┐▒┌───┐▒┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │▒│ _ │▒│ _ │ │ _ │ │ _ │ +└───┘ └───┘▒└───┘▒└───┘ └───┘ └───┘ + ▒▒▒▒▒▒▒ + ↑ + (shadow/glow) +``` + +### Background Fill + +``` +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │▓_▓│ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + ↑ + (background filled) +``` + +### Arrow Indicator + +``` +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │ _ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └─▲─┘ └───┘ └───┘ └───┘ + │ + (currently typing) +``` + +### Top Bar Indicator + +``` + ⯆ +┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ │ _ │ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ └───┘ └───┘ └───┘ └───┘ + ↑ + (top indicator) +``` + +### Multiple Indicators Combined + +``` +Frame 1: +┌───┐ ┌───┐ ╔═══╗ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ ║|_ ║ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ ╚═▲═╝ └───┘ └───┘ └───┘ + │ + (bold border + cursor + arrow) + +Frame 2: +┌───┐ ┌───┐ ╔═══╗ ┌───┐ ┌───┐ ┌───┐ +│ X │ │ Y │ ║ _ ║ │ _ │ │ _ │ │ _ │ +└───┘ └───┘ ╚═▲═╝ └───┘ └───┘ └───┘ + │ + (cursor hidden) +``` + +--- + +## Hero Section Variations + +### Center + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | ++---------------------------------------------------------+ +``` + +### Left Aligned + +``` ++---------------------------------------------------------+ +| [ Logo ] | +| CodeJam | +| Real-time Collaborative Code Editor | ++---------------------------------------------------------+ +``` + +### Minimal Inline + +``` ++---------------------------------------------------------+ +| [ Logo ] CodeJam • Real-time Collaborative Editor | ++---------------------------------------------------------+ +``` + +### Navigation + +``` ++---------------------------------------------------------+ +| [ Logo ] CodeJam Home Features Docs Login | +| | +| Real-time Collaborative Code Editor | ++---------------------------------------------------------+ +``` From 01b5182442838400d6e38d8e4719e3d9e610d202 Mon Sep 17 00:00:00 2001 From: Sanghwa Song Date: Fri, 9 Jan 2026 02:07:51 +0900 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=90=9Bfix:=20App.tsx=20=EC=9D=98=20Ro?= =?UTF-8?q?omPage=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/client/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 006de1f..e320943 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -8,7 +8,7 @@ function App() { } /> - } /> + } /> } />