Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
10c7fdb
✨ feat(files): InlineFileInput 초기값 지원 및 레이아웃 수정
inaemin Feb 5, 2026
9041d5f
✨ feat(files): 파일 이름 변경 인터페이스를 인라인 방식으로 변경
inaemin Feb 5, 2026
2d542e7
✨ feat(ui): Progress 컴포넌트 고도화 및 CapacityGauge 적용
inaemin Feb 5, 2026
b118db4
♻️ refactor(dialog): DuplicateDialog를 AlertDialog로 전환 및 기능 고도화
inaemin Feb 5, 2026
e4bdd8a
♻️ refactor(dialog): ImageUploadDialog 코드 정리 및 디자인 시스템 적용
inaemin Feb 5, 2026
0e4423e
✨ feat(files): 파일 생성 및 이름 변경 인라인 UX 완성
inaemin Feb 5, 2026
f44f0a6
🔥 remove: 헤더에서 사용하지 않는 레거시 파일 버튼 제거
inaemin Feb 5, 2026
70fcf4d
✨ feat(sidebar): 사이드바 고정(Pin) 상태 관리 및 로컬 저장소 동기화
inaemin Feb 5, 2026
829871f
✨ feat(sidebar): PinButton 컴포넌트 및 아이콘 에셋 추가
inaemin Feb 5, 2026
bdcfee8
✨ feat(sidebar): 사이드바 고정 기능 통합 및 탭별 PinButton 적용
inaemin Feb 5, 2026
98ed8c1
💄 style(sidebar): SidebarHeader 높이 및 액션 영역 정렬 수정
inaemin Feb 5, 2026
e388822
✨ feat(ui): Command 컴포넌트 추가 및 Dialog 기능 확장
inaemin Feb 5, 2026
83618ad
♻️ refactor: 방 입장 가능 여부 체크 로직 수정
inaemin Feb 5, 2026
ccabdac
♻️ refactor: ErrorDialog 위치 이동 및 UX 문구 개선
inaemin Feb 5, 2026
f67b29e
✨ feat: CustomRoomForm 스테퍼 롱프레스 연속 입력 기능 추가
inaemin Feb 5, 2026
3138e3d
♻️ refactor: FileSelectDialog를 CommandDialog 기반으로 개편
inaemin Feb 5, 2026
be44106
♻️ refactor: NicknameInputDialog를 AlertDialog로 전환 및 실시간 검증 추가
inaemin Feb 5, 2026
45019bd
♻️ refactor: PasswordInputDialog를 AlertDialog로 전환 및 UX 개선
inaemin Feb 5, 2026
f47c9bf
💄 style: 파일 목록 UI 간격 미세 조정 및 ShareDialog 트리거 수정
inaemin Feb 5, 2026
123d11c
🔥 remove: 미사용 다이얼로그 제거 및 레거시 이동
inaemin Feb 5, 2026
722f714
Merge branch 'dev' of https://github.com/boostcampwm2025/web08-JAMsta…
inaemin Feb 5, 2026
eec0c8b
✨ feat(fe): 탭 내부 인라인 액션 버튼(실행, 닫기) 추가 및 UX 개선
inaemin Feb 5, 2026
e495626
🔧 chore(fe): 헤더에서 미사용 코드 실행 버튼 관련 주석 처리
inaemin Feb 5, 2026
028fc5c
♻️ refactor(fe): 탭 내부 인라인 버튼 구현 방식 변경 (render prop 이슈 대응)
inaemin Feb 5, 2026
4150c7d
✨ feat: 파일 확장자 아이콘 SVG 에셋 추가
inaemin Feb 5, 2026
16d13cb
✨ feat: 파일 목록 및 탭에 확장자별 아이콘 적용
inaemin Feb 5, 2026
863d693
✨ feat(sidebar): 닉네임 인라인 수정을 위한 useNicknameEdit 훅 추가
inaemin Feb 6, 2026
9a53101
♻️ refactor(sidebar): 프로필 카드 디자인 개선 및 닉네임 인라인 수정 적용
inaemin Feb 6, 2026
f629f3a
💄 style(sidebar): 프로필 배너 애니메이션 디테일 조정
inaemin Feb 6, 2026
9db0472
💄 ui: 뱃지 및 라벨 스타일 최적화
inaemin Feb 6, 2026
ea7224e
💄 ui: 입력 필드 포커스 스타일 통일 및 정적 페이지 다크 모드 지원
inaemin Feb 6, 2026
7f3449e
✨ feat: 닉네임 및 비밀번호 입력 창 엔터 키 제출 지원
inaemin Feb 6, 2026
a57cb07
✨ feat: 방 참가 토큰 쿠키 저장 및 입장 상태 처리 로직 개선
inaemin Feb 6, 2026
36b9e88
✨ feat(cli): 커스텀 방 생성 시 세션 유지 지원 및 에러 메시지 고도화
inaemin Feb 6, 2026
d95276b
⚙️ setting: 최소 참가자 수 제한 변경 (1 -> 2)
inaemin Feb 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@vercel/speed-insights": "^1.3.1",
"codemirror": "^6.0.2",
"highlight.js": "^11.11.1",
"js-cookie": "^3.0.5",
"lib0": "^0.2.115",
"lucide-react": "^0.561.0",
"qrcode.react": "^4.2.0",
Expand All @@ -56,6 +57,7 @@
"devDependencies": {
"@eslint/js": "^9.39.1",
"@testing-library/react": "^16.3.1",
"@types/js-cookie": "^3.0.6",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
Expand Down
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/c.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/cpp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/java.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/javascript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/python.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/client/src/assets/exts/typescript.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions apps/client/src/assets/pin-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/client/src/assets/poly_pin.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 134 additions & 15 deletions apps/client/src/pages/home/components/CustomRoomForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
InputGroupButton,
} from '@codejam/ui';
import { ArrowLeft, Eye, EyeOff, Loader2, Minus, Plus } from 'lucide-react';
import { type ReactNode, useState } from 'react';
import {
type ReactNode,
useState,
useRef,
useCallback,
useEffect,
} from 'react';
import { ErrorMessage } from './ErrorMessage';

interface FormFieldProps {
Expand Down Expand Up @@ -68,6 +74,103 @@ function StepperField({
isInvalid,
errorMessage,
}: StepperFieldProps) {
const intervalRef = useRef<number | null>(null);
const timeoutRef = useRef<number | null>(null);
const decreaseButtonRef = useRef<HTMLButtonElement>(null);
const increaseButtonRef = useRef<HTMLButtonElement>(null);
const valueRef = useRef(value);
const minRef = useRef(min);
const maxRef = useRef(max);
const onDecreaseRef = useRef(onDecrease);
const onIncreaseRef = useRef(onIncrease);

// Keep refs in sync
useEffect(() => {
valueRef.current = value;
minRef.current = min;
maxRef.current = max;
onDecreaseRef.current = onDecrease;
onIncreaseRef.current = onIncrease;
}, [value, min, max, onDecrease, onIncrease]);

const startRepeating = useCallback((action: () => void) => {
action();
timeoutRef.current = window.setTimeout(() => {
intervalRef.current = window.setInterval(action, 100);
}, 500);
}, []);

const stopRepeating = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, []);

useEffect(() => {
const decreaseBtn = decreaseButtonRef.current;

if (decreaseBtn) {
const handleDecreaseStart = (e: PointerEvent) => {
e.preventDefault();
startRepeating(() => {
if (valueRef.current !== '' && valueRef.current > minRef.current) {
onDecreaseRef.current();
} else {
stopRepeating();
}
});
};
const handleDecreaseEnd = () => {
stopRepeating();
};

decreaseBtn.addEventListener('pointerdown', handleDecreaseStart);
decreaseBtn.addEventListener('pointerup', handleDecreaseEnd);
decreaseBtn.addEventListener('pointercancel', handleDecreaseEnd);

return () => {
decreaseBtn.removeEventListener('pointerdown', handleDecreaseStart);
decreaseBtn.removeEventListener('pointerup', handleDecreaseEnd);
decreaseBtn.removeEventListener('pointercancel', handleDecreaseEnd);
};
}
}, [startRepeating, stopRepeating]);

useEffect(() => {
const increaseBtn = increaseButtonRef.current;

if (increaseBtn) {
const handleIncreaseStart = (e: PointerEvent) => {
e.preventDefault();
startRepeating(() => {
if (valueRef.current === '' || valueRef.current < maxRef.current) {
onIncreaseRef.current();
} else {
stopRepeating();
}
});
};
const handleIncreaseEnd = () => {
stopRepeating();
};

increaseBtn.addEventListener('pointerdown', handleIncreaseStart);
increaseBtn.addEventListener('pointerup', handleIncreaseEnd);
increaseBtn.addEventListener('pointercancel', handleIncreaseEnd);

return () => {
increaseBtn.removeEventListener('pointerdown', handleIncreaseStart);
increaseBtn.removeEventListener('pointerup', handleIncreaseEnd);
increaseBtn.removeEventListener('pointercancel', handleIncreaseEnd);
};
}
}, [startRepeating, stopRepeating]);

return (
<div className={className}>
<FormField title={title} description={description}>
Expand All @@ -81,20 +184,22 @@ function StepperField({
className="w-14 [appearance:textfield] text-center [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
/>
<Button
ref={decreaseButtonRef}
variant="outline"
size="icon"
onClick={onDecrease}
disabled={value === '' || value <= min}
type="button"
style={{ touchAction: 'none' }}
>
<Minus className="size-4" />
</Button>
<Button
ref={increaseButtonRef}
variant="outline"
size="icon"
onClick={onIncrease}
disabled={value !== '' && value >= max}
type="button"
style={{ touchAction: 'none' }}
>
<Plus className="size-4" />
</Button>
Expand Down Expand Up @@ -263,19 +368,33 @@ export function CustomRoomForm({
}
};

const decreaseMaxPts = () => {
const current = maxPtsInput === '' ? LIMITS.MIN_PTS : maxPtsInput;
const newVal = Math.max(current - 1, LIMITS.MIN_PTS);
setMaxPtsInput(newVal);
handleChange('maxPts', newVal);
};
const decreaseMaxPts = useCallback(() => {
setMaxPtsInput((prev) => {
const current = prev === '' ? LIMITS.MIN_PTS : prev;
if (current <= LIMITS.MIN_PTS) {
return current;
}
const newVal = Math.max(current - 1, LIMITS.MIN_PTS);
queueMicrotask(() => {
setCustomRoomConfig((c) => ({ ...c, maxPts: newVal }));
});
return newVal;
});
}, []);

const increaseMaxPts = () => {
const current = maxPtsInput === '' ? LIMITS.MIN_PTS : maxPtsInput;
const newVal = Math.min(current + 1, LIMITS.MAX_PTS);
setMaxPtsInput(newVal);
handleChange('maxPts', newVal);
};
const increaseMaxPts = useCallback(() => {
setMaxPtsInput((prev) => {
const current = prev === '' ? LIMITS.MIN_PTS : prev;
if (current >= LIMITS.MAX_PTS) {
return current;
}
const newVal = Math.min(current + 1, LIMITS.MAX_PTS);
queueMicrotask(() => {
setCustomRoomConfig((c) => ({ ...c, maxPts: newVal }));
});
return newVal;
});
}, []);

const handleRoomPasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange('roomPassword', e.target.value);
Expand Down
24 changes: 19 additions & 5 deletions apps/client/src/pages/join/JoinPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
import { ROUTES } from '@codejam/common';
import { ROUTES, ROOM_CONFIG } from '@codejam/common';
import { Loader2 } from 'lucide-react';
import Cookies from 'js-cookie';

export default function JoinPage() {
const { roomCode, token } = useLoaderData() as {
Expand All @@ -11,19 +12,32 @@ export default function JoinPage() {
const navigate = useNavigate();

useEffect(() => {
// Navigate to room immediately
// Save token to cookie
const cookieName = `auth_${roomCode.toUpperCase()}`;
const isProduction = import.meta.env.PROD;

Cookies.set(cookieName, token, {
expires: ROOM_CONFIG.COOKIE_MAX_AGE / (1000 * 60 * 60 * 24), // Convert ms to days
path: '/',
secure: isProduction,
sameSite: isProduction ? 'none' : 'lax',
});

// Navigate to room
const url = ROUTES.ROOM(roomCode);
navigate(url, { replace: true });
}, [roomCode, token, navigate]);

return (
<main className="flex h-screen flex-col items-center justify-center bg-gray-100">
<main className="flex h-screen flex-col items-center justify-center bg-gray-100 dark:bg-gray-900">
<section className="flex flex-col items-center text-center">
<Loader2 className="text-primary mb-4 h-16 w-16 animate-spin" />
<h1 className="mb-4 text-2xl font-semibold text-gray-800">
<h1 className="mb-4 text-2xl font-semibold text-gray-800 dark:text-gray-100">
방 참가 중
</h1>
<p className="mb-8 text-gray-500">잠시만 기다려 주세요...</p>
<p className="mb-8 text-gray-500 dark:text-gray-400">
잠시만 기다려 주세요...
</p>
</section>
</main>
);
Expand Down
8 changes: 5 additions & 3 deletions apps/client/src/pages/not-found/NotFoundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ function NotFoundPage() {
}

return (
<main className="flex h-screen flex-col items-center justify-center bg-gray-100">
<main className="flex h-screen flex-col items-center justify-center bg-gray-100 dark:bg-gray-900">
<section className="flex flex-col items-center text-center">
<span className="mb-4 text-6xl" role="img" aria-label="에러 아이콘">
{icon}
</span>
<h1 className="mb-4 text-2xl font-semibold text-gray-800">{title}</h1>
<p className="mb-8 text-gray-500">{message}</p>
<h1 className="mb-4 text-2xl font-semibold text-gray-800 dark:text-gray-100">
{title}
</h1>
<p className="mb-8 text-gray-500 dark:text-gray-400">{message}</p>
<nav>
<Button size="lg" render={<Link to="/" />}>
메인 페이지로 이동
Expand Down
17 changes: 5 additions & 12 deletions apps/client/src/pages/room/RoomPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ import { useRoomJoin } from '@/shared/lib/hooks/useRoomJoin';
import { RadixToaster as Toaster } from '@codejam/ui';
import { useFileStore } from '@/stores/file';
import { useLoaderData } from 'react-router-dom';
import { ErrorDialog } from '@/widgets/error-dialog/ErrorDialog';
import { ErrorDialog } from '@/widgets/dialog/ErrorDialog';
import { HostClaimRequestDialog } from '@/widgets/dialog/HostClaimRequestDialog';
import { PERMISSION, type RoomJoinStatus } from '@codejam/common';
import { usePermission } from '@/shared/lib/hooks/usePermission';
import { PrepareStage } from './PrepareStage';
import { useAwarenessSync } from '@/shared/lib/hooks/useAwarenessSync';
import { useInitialFileSelection } from '@/shared/lib/hooks/useInitialFileSelection';
import { useFileRename } from '@/shared/lib/hooks/useFileRename';
import { DuplicateDialog } from '@/widgets/dialog/DuplicateDialog';
import { ConsolePanel as Output } from '@/widgets/console';
import { useDarkMode } from '@/shared/lib/hooks/useDarkMode';
import { Chat } from '@/widgets/chat';
Expand All @@ -37,9 +36,7 @@ function RoomPage() {
handlePasswordConfirm,
} = useRoomJoin();

const { setIsDuplicated, isDuplicated, handleFileChange } = useFileRename(
paramCode!,
);
const { handleFileChange } = useFileRename(paramCode!);

useAwarenessSync();
useInitialFileSelection();
Expand Down Expand Up @@ -122,9 +119,9 @@ function RoomPage() {
</div>
{loader === 'FULL' ? (
<ErrorDialog
title="사람이 가득 찼습니다!"
description="현재 방에 인원이 많습니다."
buttonLabel="뒤로가기"
title="방이 가득 찼어요"
description="현재 방에 참여할 수 있는 최대 인원에 도달했어요."
buttonLabel="홈으로 돌아가기"
onSubmit={() => {
window.location.href = '/';
}}
Expand All @@ -141,10 +138,6 @@ function RoomPage() {
/>
)}
<Toaster richColors position="top-center" />
<DuplicateDialog
open={isDuplicated}
onOpenChange={setIsDuplicated}
/>
<HostClaimRequestDialog />
<ShortcutHUD />
</div>
Expand Down
Loading
Loading