- {streamedLogLines.map((line, idx) => (
-
- {typeof line === 'string' ? line.trimStart() : ''}
-
- ))}
+ const logsOutput = (
+
+ {streamedLogLines.map((line, idx) => (
+
+ {typeof line === 'string' ? line.trimStart() : ''}
- ),
- timestamp: new Date(),
- },
- ]);
- }, [streamedLogLines]);
+ ))}
+
+ );
+
+ if (props.enableCollaboration && isConnected) {
+ console.log(`[CodeRunner] νμ
λͺ¨λ λ‘κ·Έ μΆλ ₯ λΈλ‘λμΊμ€νΈ`);
+ broadcastCommand('logs', `Logs displayed (${streamedLogLines.length} lines)`, new Date());
+ } else {
+ console.log(`[CodeRunner] λ‘컬 λͺ¨λ λ‘κ·Έ μΆλ ₯ μ μ₯`);
+ setLocalCommandHistory(prev => [
+ ...prev.filter(item => item.command !== 'logs'),
+ {
+ command: 'logs',
+ output: logsOutput,
+ timestamp: new Date(),
+ },
+ ]);
+ }
+ }, [streamedLogLines, props.enableCollaboration, isConnected, broadcastCommand]);
+ // π§ μμ : stop μ΄μ€ μ²λ¦¬ λ¬Έμ ν΄κ²°
const handleStop = () => {
- setCommandHistory(prev => [
- ...prev,
- { command: 'stop', output: 'μ€μ§ μ€...', timestamp: new Date() },
- ]);
+ console.log(`[CodeRunner] μ€μ§ λͺ
λ Ήμ΄ μ€ν`);
+
+ // π§ μ¦μ λΈλ‘λμΊμ€νΈνμ§ μκ³ API νΈμΆλ§
codeRunnerStop.mutate(undefined, {
onSuccess: resp => {
- setCommandHistory(prev => [
- ...prev.slice(0, -1),
- {
- command: 'stop',
- output: resp.message,
- timestamp: new Date(),
- },
- ]);
+ console.log(`[CodeRunner] μ€μ§ μ±κ³΅:`, resp);
+
+ // π§ μ±κ³΅ μμλ§ λΈλ‘λμΊμ€νΈ/μ μ₯
+ if (props.enableCollaboration && isConnected) {
+ broadcastCommand('stop', resp.message, new Date());
+ } else {
+ setLocalCommandHistory(prev => [
+ ...prev,
+ {
+ command: 'stop',
+ output: resp.message,
+ timestamp: new Date(),
+ },
+ ]);
+ }
},
onError: e => {
+ console.error(`[CodeRunner] μ€μ§ μ€ν¨:`, e);
+
let errorMessage = 'μ€μ§ μ€ν¨';
if (typeof e === 'object' && e !== null) {
const err = e as { response?: { data?: { message?: string } }; message?: string };
errorMessage = err.response?.data?.message || err.message || 'μ€μ§ μ€ν¨';
}
- setCommandHistory(prev => [
- ...prev.slice(0, -1),
- {
- command: 'stop',
- output: errorMessage,
- timestamp: new Date(),
- },
- ]);
+
+ // π§ μ€ν¨ μμλ§ λΈλ‘λμΊμ€νΈ/μ μ₯
+ if (props.enableCollaboration && isConnected) {
+ broadcastCommand('stop', errorMessage, new Date());
+ } else {
+ setLocalCommandHistory(prev => [
+ ...prev,
+ {
+ command: 'stop',
+ output: errorMessage,
+ timestamp: new Date(),
+ },
+ ]);
+ }
},
});
};
@@ -215,9 +297,15 @@ export function CodeRunner(props: CodeRunnerProps) {
}
}, [commandHistory]);
+ const getConnectionStatusText = () => {
+ if (!props.enableCollaboration) return null;
+ if (collaborationError) return 'μ°κ²° μ€λ₯';
+ if (isConnected) return 'μ°κ²°λ¨';
+ return 'μ°κ²° μ€...';
+ };
+
return (
- {/* μ μ΄ μΉμ
*/}
+ {props.enableCollaboration && (
+
+ {collaborationError ? '!' : isConnected ? 'β' : 'β'}
+
+ )}
- {/* ν°λ―Έλ μ½ν
μΈ */}
+ {props.enableCollaboration && (
+
+ {getConnectionStatusText()}
+ {/* π§ λλ²κ·Έ μ 보 μΆκ° */}
+
+ User: {props.userId} ({props.userName})
+
+
+ )}
+
{commandHistory.map((item, index) => (
diff --git a/src/features/CodeRunner/types.ts b/src/features/CodeRunner/types.ts
index a7f8da45..97fdda67 100644
--- a/src/features/CodeRunner/types.ts
+++ b/src/features/CodeRunner/types.ts
@@ -1,14 +1,17 @@
+import type { ReactNode } from 'react';
+
export interface CommandHistory {
command: string;
- output: string;
+ output: string | ReactNode;
timestamp: Date;
}
export interface CodeRunnerProps {
- repoId?: string;
- onCommandExecute?: (command: string) => Promise
| string;
- onClose?: () => void;
- initialHistory?: CommandHistory[];
+ repoId?: number | string;
+ repositoryName?: string;
+ enableCollaboration?: boolean;
+ userId?: string;
+ userName?: string;
}
export interface CodeRunnerState {
diff --git a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss
index 66d6b777..ad19d0df 100644
--- a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss
+++ b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.module.scss
@@ -49,6 +49,37 @@
}
}
+ // ν μνλ³ λ―Έλ¬ν μ€νμΌ λ³κ²½
+ &.hasTab {
+ // νμ΄ μ΄λ¦° νμΌ - λ―Έλ¬νκ² κ°μ‘°
+ &.file {
+ font-weight: $font-weight-medium;
+ }
+ }
+
+ &.activeTab {
+ // νμ¬ νμ± νμΈ νμΌ - νΈλ² μνμ λμΌν κΈμμ + λ―Έλ¬ν λ°°κ²½μ
+ &.file {
+ color: var(--filetree-text-hover); // νΈλ² μνμ λμΌν μμ
+ background-color: var(--filetree-active-tab-subtle-bg);
+
+ &:hover {
+ background-color: var(--filetree-active-tab-subtle-hover);
+ }
+
+ // μμ΄μ½λ νΈλ² μνμ λμΌνκ²
+ .icon {
+ opacity: 1;
+ transform: scale(1.05);
+
+ // λ€ν¬λͺ¨λμμλ νΈλ² μνμ λμΌ
+ :global(.dark) & {
+ filter: brightness(0) invert(1) brightness(0.9) contrast(1.2);
+ }
+ }
+ }
+ }
+
// νμΌκ³Ό ν΄λμ λ°λ₯Έ μ€νμΌ μ°¨μ΄
&.folder {
font-weight: $font-weight-medium;
@@ -365,6 +396,38 @@
}
}
+// ν μν μΈλμΌμ΄ν° 컨ν
μ΄λ - μ°μΈ‘ λμ κ³ μ
+.tabStatusIndicators {
+ position: absolute;
+ top: 50%;
+ right: 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ transform: translateY(-50%);
+ pointer-events: none;
+}
+
+// ν μν μΈλμΌμ΄ν°λ€ - μ°μΈ‘ λ κ³ μ μμΉ
+.activeIndicator {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background-color: var(--filetree-tab-active-dot);
+ box-shadow: 0 0 4px var(--filetree-tab-active-glow);
+ animation: subtle-pulse 2s ease-in-out infinite;
+}
+
+.dirtyIndicator {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background-color: var(--filetree-tab-dirty-dot);
+ box-shadow: 0 0 3px var(--filetree-tab-dirty-glow);
+ animation: dirty-pulse 1.5s ease-in-out infinite;
+}
+
// νμΌ/ν΄λ μ΄λ¦
.name {
flex: 0 1 auto;
@@ -498,6 +561,33 @@
}
}
+// ν μν μ λλ©μ΄μ
- μμ£Ό λ―Έλ¬ν¨
+@keyframes subtle-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 0.6;
+ transform: scale(0.8);
+ }
+}
+
+@keyframes dirty-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: 0.7;
+ transform: scale(0.9);
+ }
+}
+
// μλμͺ½μ λνλλ ν΄νμ© μ λλ©μ΄μ
@keyframes tooltip-appear-below {
from {
@@ -540,6 +630,14 @@
--filetree-selected-indicator: #{$orange};
--filetree-editing-indicator: #{$yellow};
+ // ν μν μμ - λ―Έλ¬ν¨
+ --filetree-active-tab-subtle-bg: rgb(55 79 255 / 3%);
+ --filetree-active-tab-subtle-hover: rgb(55 79 255 / 6%);
+ --filetree-tab-active-dot: #{$blue-3};
+ --filetree-tab-active-glow: rgb(55 79 255 / 40%);
+ --filetree-tab-dirty-dot: #{$orange};
+ --filetree-tab-dirty-glow: rgb(255 107 53 / 40%);
+
// λ΄λΆ λλκ·Έμ€λλ‘ μμ
--filetree-item-dragging: #{$gray-8};
--filetree-drag-border: #{$blue-3};
@@ -590,6 +688,14 @@
--filetree-selected-indicator: #{$orange};
--filetree-editing-indicator: #{$yellow};
+ // ν μν μμ - λ―Έλ¬ν¨
+ --filetree-active-tab-subtle-bg: rgb(55 79 255 / 8%);
+ --filetree-active-tab-subtle-hover: rgb(55 79 255 / 12%);
+ --filetree-tab-active-dot: #{$blue-3};
+ --filetree-tab-active-glow: rgb(55 79 255 / 60%);
+ --filetree-tab-dirty-dot: #{$orange};
+ --filetree-tab-dirty-glow: rgb(255 107 53 / 60%);
+
// λ΄λΆ λλκ·Έμ€λλ‘ μμ
--filetree-item-dragging: #{$gray-5};
--filetree-drag-border: #{$blue-3};
diff --git a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx
index 7dea01e1..bb29781a 100644
--- a/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx
+++ b/src/features/Repo/fileTree/components/FileTreeItem/FileTreeItem.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import clsx from 'clsx';
import { getFolderIcon, getFileIcon } from '@/utils/fileExtensions';
+import { useTabStore } from '@/stores/tabStore';
import FileTreeContextMenu from '../FileTreeContextMenu/FileTreeContextMenu';
import InlineEdit from '../InlineEdit/InlineEdit';
import styles from './FileTreeItem.module.scss';
@@ -38,6 +39,29 @@ const FileTreeItem: React.FC = ({
onExternalDragLeave,
onExternalDrop,
}) => {
+ const { openTabs } = useTabStore();
+
+ // ν μν νμΈ - νμΌλ§ 체ν¬
+ const getTabStatus = () => {
+ if (node.fileType === 'FOLDER') {
+ return { isOpen: false, isActive: false, isDirty: false };
+ }
+
+ const tab = openTabs.find(
+ tab =>
+ tab.fileId === node.fileId || tab.path === node.path || tab.id.endsWith(`/${node.path}`)
+ );
+
+ return {
+ isOpen: !!tab && !tab.isDeleted && !tab.hasFileTreeMismatch,
+ isActive: !!tab && tab.isActive && !tab.isDeleted && !tab.hasFileTreeMismatch,
+ isDirty: !!tab && tab.isDirty && !tab.isDeleted && !tab.hasFileTreeMismatch,
+ tab,
+ };
+ };
+
+ const tabStatus = getTabStatus();
+
const handleClick = (e: React.MouseEvent) => {
// νΈμ§ μ€μ΄κ±°λ λλκ·Έ μ€μΌ λλ ν΄λ¦ μ΄λ²€νΈ 무μ
if (isEditing || isDragging) {
@@ -242,6 +266,10 @@ const FileTreeItem: React.FC = ({
[styles.canDrop]: canDrop && isDropTarget,
[styles.cannotDrop]: !canDrop && isDropTarget,
[styles.draggable]: !isEditing,
+ // ν μν ν΄λμ€ - λ―Έλ¬νκ² μ μ©
+ [styles.hasTab]: tabStatus.isOpen,
+ [styles.activeTab]: tabStatus.isActive,
+ [styles.dirtyTab]: tabStatus.isDirty,
// λ΄λΆ λλ‘ μμΉλ³ ν΄λμ€
[styles.dropBefore]:
isDropTarget && getDropPosition?.(node.fileId.toString()) === 'before',
@@ -270,6 +298,11 @@ const FileTreeItem: React.FC = ({
onDrop={handleCombinedDrop}
// μ΅μλ¨ λ 벨 μ¬λΆλ₯Ό data attributeλ‘ μ λ¬
data-is-top-level={isTopLevel}
+ title={
+ tabStatus.isOpen
+ ? `${node.fileName}${tabStatus.isActive ? ' (νμ± ν)' : ' (μ΄λ¦° ν)'}${tabStatus.isDirty ? ' (λ³κ²½λ¨)' : ''}`
+ : node.fileName
+ }
>
{node.fileType === 'FOLDER' && (
@@ -309,6 +342,16 @@ const FileTreeItem: React.FC
= ({
validateInput={validateFileName}
/>
+ {/* ν μν μΈλμΌμ΄ν° - μ°μΈ‘ λμ κ³ μ μμΉ */}
+ {node.fileType === 'FILE' && (
+
+ {tabStatus.isActive &&
}
+ {tabStatus.isDirty && (
+
+ )}
+
+ )}
+
{/* μΈλΆ νμΌ λλκ·Έμ€λ² μν νμ */}
{isExternalDragOver && (
diff --git a/src/hooks/chat/useWebSocketChat.ts b/src/hooks/chat/useWebSocketChat.ts
deleted file mode 100644
index dd5232f8..00000000
--- a/src/hooks/chat/useWebSocketChat.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-import { useEffect, useRef, useCallback, useState } from 'react';
-import { getWebSocketConfig, buildWebSocketUrl, wsLogger } from '@/utils/websocketConfig';
-
-export interface ChatMessage {
- id: string;
- userId: string;
- userName: string;
- profileImageUrl: string;
- content: string;
- timestamp: number;
- type: 'message' | 'system';
-}
-
-interface WebSocketMessage {
- type: 'message' | 'join' | 'leave' | 'user_list' | 'message_history';
- data:
- | ChatMessage
- | ChatMessage[]
- | { userId: string; userName: string }
- | Record
;
- roomId?: string;
- userId?: string;
- token?: string;
-}
-
-interface UseWebSocketChatProps {
- roomId: string;
- userId: string;
- userName: string;
- profileImageUrl: string;
- enabled?: boolean;
-}
-
-interface UseWebSocketChatReturn {
- messages: ChatMessage[];
- sendMessage: (content: string) => void;
- isConnected: boolean;
- isLoading: boolean;
- onlineUsers: Array<{ userId: string; userName: string }>;
-}
-
-export const useWebSocketChat = ({
- roomId,
- userId,
- userName,
- profileImageUrl,
- enabled = true,
-}: UseWebSocketChatProps): UseWebSocketChatReturn => {
- const wsRef = useRef(null);
- const reconnectTimeoutRef = useRef(null);
- const isConnectingRef = useRef(false);
- const [messages, setMessages] = useState([]);
- const [isConnected, setIsConnected] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [onlineUsers, setOnlineUsers] = useState>([]);
-
- const cleanup = useCallback(() => {
- wsLogger.info('WebSocket Chat μ°κ²° μ 리 μ€...');
-
- if (reconnectTimeoutRef.current) {
- clearTimeout(reconnectTimeoutRef.current);
- reconnectTimeoutRef.current = null;
- }
-
- if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
- if (wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(
- JSON.stringify({
- type: 'leave',
- data: { userId, userName },
- roomId,
- userId,
- })
- );
- }
- wsRef.current.close();
- wsRef.current = null;
- }
-
- setIsConnected(false);
- setIsLoading(false);
- isConnectingRef.current = false;
- }, [userId, userName, roomId]);
-
- const sendWebSocketMessage = useCallback((message: WebSocketMessage) => {
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.send(JSON.stringify(message));
- }
- }, []); // dependency μ κ±° - wsRefλ refμ΄λ―λ‘ μμ μ
-
- const handleWebSocketMessage = useCallback((event: MessageEvent) => {
- try {
- const message: WebSocketMessage = JSON.parse(event.data);
-
- switch (message.type) {
- case 'message':
- if (message.data && typeof message.data === 'object' && 'id' in message.data) {
- const chatMessage = message.data as ChatMessage;
- setMessages(prev => {
- if (prev.some(msg => msg.id === chatMessage.id)) {
- return prev;
- }
- return [...prev, chatMessage].sort((a, b) => a.timestamp - b.timestamp);
- });
- }
- break;
-
- case 'message_history':
- if (Array.isArray(message.data)) {
- const historyMessages = message.data as ChatMessage[];
- setMessages(historyMessages.sort((a, b) => a.timestamp - b.timestamp));
- }
- break;
-
- case 'user_list':
- if (Array.isArray(message.data)) {
- setOnlineUsers(message.data);
- }
- break;
-
- case 'join':
- case 'leave':
- wsLogger.info(`μ¬μ©μ ${message.type}:`, message.data);
- break;
-
- default:
- wsLogger.warn('μ μ μλ λ©μμ§ νμ
:', message);
- }
- } catch (error) {
- wsLogger.error('WebSocket λ©μμ§ νμ± μ€λ₯:', error);
- }
- }, []);
-
- const connect = useCallback(() => {
- if (!roomId || !enabled || isConnectingRef.current) {
- return;
- }
-
- // μ΄λ―Έ μ°κ²°λμ΄ μλ€λ©΄ μλ‘ μ°κ²°νμ§ μμ
- if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
- return;
- }
-
- isConnectingRef.current = true;
-
- // κΈ°μ‘΄ μ°κ²°μ΄ μλ€λ©΄ λ¨Όμ μ 리
- if (wsRef.current) {
- if (wsRef.current.readyState === WebSocket.OPEN) {
- wsRef.current.close();
- }
- wsRef.current = null;
- }
-
- try {
- wsLogger.info('WebSocket Chat μ°κ²° μμ:', roomId);
- setIsLoading(true);
-
- const config = getWebSocketConfig();
- const token = sessionStorage.getItem('accessToken');
-
- const wsUrl = buildWebSocketUrl(config.chatWsUrl, {
- roomId,
- userId,
- userName: encodeURIComponent(userName),
- ...(token && { token }),
- });
-
- const ws = new WebSocket(wsUrl);
- wsRef.current = ws;
-
- ws.onopen = () => {
- wsLogger.info('WebSocket Chat μ°κ²° μ±κ³΅');
- setIsConnected(true);
- setIsLoading(false);
- isConnectingRef.current = false;
-
- // sendWebSocketMessage ν¨μλ₯Ό μ§μ νΈμΆνμ§ μκ³ μΈλΌμΈμΌλ‘ μ²λ¦¬
- if (ws.readyState === WebSocket.OPEN) {
- ws.send(
- JSON.stringify({
- type: 'join',
- data: { userId, userName },
- roomId,
- userId,
- token: token || undefined,
- })
- );
- }
- };
-
- ws.onmessage = handleWebSocketMessage;
-
- ws.onclose = event => {
- wsLogger.warn('WebSocket Chat μ°κ²° μ’
λ£:', event.code, event.reason);
- setIsConnected(false);
- setIsLoading(false);
- wsRef.current = null;
- isConnectingRef.current = false;
-
- // μ μ μ’
λ£κ° μλ κ²½μ°λ§ μ¬μ°κ²° μλ
- if (enabled && event.code !== 1000) {
- const config = getWebSocketConfig();
- setTimeout(() => {
- connect();
- }, config.reconnectOptions.delay);
- }
- };
-
- ws.onerror = error => {
- wsLogger.error('WebSocket Chat μ€λ₯:', error);
- setIsConnected(false);
- setIsLoading(false);
- isConnectingRef.current = false;
- };
- } catch (error) {
- wsLogger.error('WebSocket Chat μ°κ²° μ€ν¨:', error);
- setIsLoading(false);
- isConnectingRef.current = false;
- }
- }, [roomId, enabled, userId, userName, handleWebSocketMessage]); // dependency μ΅μ ν
-
- const sendMessage = useCallback(
- (content: string) => {
- if (!wsRef.current || !content.trim() || wsRef.current.readyState !== WebSocket.OPEN) {
- return;
- }
-
- const newMessage: ChatMessage = {
- id: `${userId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
- userId,
- userName,
- profileImageUrl,
- content: content.trim(),
- timestamp: Date.now(),
- type: 'message',
- };
-
- sendWebSocketMessage({
- type: 'message',
- data: newMessage,
- roomId,
- userId,
- token: sessionStorage.getItem('accessToken') || undefined,
- });
- },
- [userId, userName, profileImageUrl, roomId, sendWebSocketMessage]
- );
-
- // useRefλ‘ μ΅μ κ°λ€μ μμ μ μΌλ‘ μ°Έμ‘°
- const latestParamsRef = useRef({ enabled, roomId, userId, userName });
- useEffect(() => {
- latestParamsRef.current = { enabled, roomId, userId, userName };
- });
-
- useEffect(() => {
- const { enabled, roomId, userId } = latestParamsRef.current;
- if (enabled && roomId && userId) {
- connect();
- }
- return cleanup;
- }, [enabled, roomId, userId, connect, cleanup]);
-
- return {
- messages,
- sendMessage,
- isConnected,
- isLoading,
- onlineUsers,
- };
-};
diff --git a/src/hooks/repo/useFileContentLoader.ts b/src/hooks/repo/useFileContentLoader.ts
index 2750b9bc..23ed388f 100644
--- a/src/hooks/repo/useFileContentLoader.ts
+++ b/src/hooks/repo/useFileContentLoader.ts
@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
import { useTabStore } from '@/stores/tabStore';
import { useFileContent } from './useFileContent';
@@ -17,6 +17,9 @@ export const useFileContentLoader = ({
}: UseFileContentLoaderParams) => {
const { openTabs, setTabContentFromFile } = useTabStore();
+ // μ€λ³΅ μ²λ¦¬ λ°©μ§λ₯Ό μν ref
+ const processedTabsRef = useRef>(new Set());
+
const activeTab = openTabs.find(tab => tab.isActive);
console.log('FileContentLoader μν:', {
@@ -27,19 +30,15 @@ export const useFileContentLoader = ({
shouldLoad: enabled && !enableCollaboration && activeTab && activeTab.content === '',
});
- // λ‘λ© μ‘°κ±΄:
- // 1. enabledκ° true
- // 2. νμ
λͺ¨λκ° μλ (νμ
λͺ¨λμμλ νμΌνΈλ¦¬μμ μ§μ λ‘λ)
- // 3. νμ± νμ΄ μμ
- // 4. ν λ΄μ©μ΄ λΉμ΄μμ (μμ§ λ‘λλμ§ μμ)
- // 5. νμ΄ νμ¬ λ ν¬μ κ²μ
+ // λ‘λ© μ‘°κ±΄ + μ€λ³΅ μ²λ¦¬ λ°©μ§
const shouldLoadContent =
enabled &&
!enableCollaboration &&
activeTab &&
activeTab.content === '' &&
activeTab.id.startsWith(`${repoId}/`) &&
- activeTab.fileId;
+ activeTab.fileId &&
+ !processedTabsRef.current.has(activeTab.id); // μ€λ³΅ μ²λ¦¬ λ°©μ§ μΆκ°
const {
data: fileContentData,
@@ -49,7 +48,7 @@ export const useFileContentLoader = ({
} = useFileContent({
repositoryId,
fileId: activeTab?.fileId || 0,
- enabled: Boolean(shouldLoadContent), // BooleanμΌλ‘ λ³ννμ¬ νμ
μλ¬ ν΄κ²°
+ enabled: Boolean(shouldLoadContent),
});
// νμΌ λ΄μ© λ‘λ μ±κ³΅ μ νμ μ€μ
@@ -58,6 +57,11 @@ export const useFileContentLoader = ({
const tabId = activeTab.id;
const content = fileContentData.data.content;
+ // μ΄λ―Έ μ²λ¦¬ν νμΈμ§ νμΈ
+ if (processedTabsRef.current.has(tabId)) {
+ return;
+ }
+
console.log('νμΌ λ΄μ© μ€μ (μΌλ° λͺ¨λ):', {
tabId,
filePath: activeTab.path,
@@ -66,14 +70,33 @@ export const useFileContentLoader = ({
fileId: activeTab.fileId,
});
+ // μ²λ¦¬ μλ£ νμ
+ processedTabsRef.current.add(tabId);
+
// setTabContentFromFile μ¬μ©μΌλ‘ clean μν 보μ₯
setTabContentFromFile(tabId, content);
}
- }, [fileContentData, activeTab, shouldLoadContent, setTabContentFromFile]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ fileContentData?.data?.content,
+ activeTab?.id,
+ activeTab?.path,
+ activeTab?.name,
+ activeTab?.fileId,
+ shouldLoadContent,
+ setTabContentFromFile,
+ ]);
// μλ¬ μ²λ¦¬
useEffect(() => {
if (error && activeTab && shouldLoadContent) {
+ const tabId = activeTab.id;
+
+ // μ΄λ―Έ μ²λ¦¬ν νμΈμ§ νμΈ
+ if (processedTabsRef.current.has(tabId)) {
+ return;
+ }
+
console.error('νμΌ λ΄μ© λ‘λ μλ¬:', {
error,
tabId: activeTab.id,
@@ -93,9 +116,34 @@ export const useFileContentLoader = ({
// λ¬Έμ κ° μ§μλλ©΄ νμ΄μ§λ₯Ό μλ‘κ³ μΉ¨ν΄μ£ΌμΈμ.`;
+ // μ²λ¦¬ μλ£ νμ
+ processedTabsRef.current.add(tabId);
setTabContentFromFile(activeTab.id, errorMessage);
}
- }, [error, activeTab, shouldLoadContent, setTabContentFromFile]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ error,
+ activeTab?.id,
+ activeTab?.path,
+ activeTab?.fileId,
+ shouldLoadContent,
+ setTabContentFromFile,
+ ]);
+
+ // νμ΄ λ³κ²½λλ©΄ μ²λ¦¬λ ν λͺ©λ‘ μ 리
+ useEffect(() => {
+ const currentTabIds = openTabs.map(tab => tab.id);
+ const newProcessedTabs = new Set();
+
+ // νμ¬ μ΄λ¦° νλ€λ§ μ μ§
+ currentTabIds.forEach(tabId => {
+ if (processedTabsRef.current.has(tabId)) {
+ newProcessedTabs.add(tabId);
+ }
+ });
+
+ processedTabsRef.current = newProcessedTabs;
+ }, [openTabs]);
// μλ μλ‘κ³ μΉ¨ ν¨μ
const refreshCurrentFile = () => {
@@ -105,6 +153,8 @@ export const useFileContentLoader = ({
filePath: activeTab.path,
});
+ // μ²λ¦¬ κΈ°λ‘ μ κ±°νμ¬ λ€μ λ‘λ κ°λ₯νκ² ν¨
+ processedTabsRef.current.delete(activeTab.id);
refetch();
}
};
diff --git a/src/hooks/repo/useYjsCodeRunner.ts b/src/hooks/repo/useYjsCodeRunner.ts
new file mode 100644
index 00000000..2d2718e5
--- /dev/null
+++ b/src/hooks/repo/useYjsCodeRunner.ts
@@ -0,0 +1,412 @@
+import { useEffect, useRef, useCallback, useState } from 'react';
+import * as Y from 'yjs';
+import { WebsocketProvider } from 'y-websocket';
+import { useCollaborationStore } from '@/stores/collaborationStore';
+import type { CommandHistory } from '@/features/CodeRunner/types';
+
+interface CodeRunnerCollaborationConfig {
+ roomId: string;
+ userId: string;
+ userName: string;
+ enabled?: boolean;
+}
+
+interface CodeRunnerConnectionData {
+ doc: Y.Doc;
+ provider: WebsocketProvider;
+ yArray: Y.Array;
+ activeUsers: Set;
+ cleanupTimer?: NodeJS.Timeout;
+ reconnectAttempts: number;
+ maxReconnectAttempts: number;
+ isDestroyed: boolean;
+}
+
+const codeRunnerConnections = new Map();
+
+const getWebSocketUrl = (): string => {
+ return import.meta.env.VITE_YJS_WEBSOCKET_URL || 'ws://localhost:1234';
+};
+
+const cleanupCodeRunnerConnection = (roomId: string) => {
+ const connection = codeRunnerConnections.get(roomId);
+ if (!connection) return;
+
+ console.log(`[CodeRunner] μ°κ²° μ 리: ${roomId}`);
+
+ try {
+ connection.isDestroyed = true;
+
+ if (connection.cleanupTimer) {
+ clearTimeout(connection.cleanupTimer);
+ }
+
+ connection.provider.disconnect();
+ connection.provider.destroy();
+ connection.doc.destroy();
+ codeRunnerConnections.delete(roomId);
+
+ console.log(`[CodeRunner] μ°κ²° μ 리 μλ£: ${roomId}`);
+ } catch (error) {
+ console.error(`[CodeRunner] μ°κ²° μ 리 μ€ν¨: ${roomId}`, error);
+ }
+};
+
+export const useYjsCodeRunner = ({
+ roomId,
+ userId,
+ userName,
+ enabled = true,
+}: CodeRunnerCollaborationConfig) => {
+ const [isConnected, setIsConnected] = useState(false);
+ const [error, setError] = useState(null);
+ const [commandHistory, setCommandHistory] = useState([]);
+
+ const currentUserIdRef = useRef('');
+ const isInitializedRef = useRef(false);
+ const cleanupInProgressRef = useRef(false);
+ const reconnectTimeoutRef = useRef(null);
+
+ const { setConnectionStatus, addUser, clearUsers } = useCollaborationStore();
+
+ const cleanup = useCallback(() => {
+ if (cleanupInProgressRef.current) return;
+ cleanupInProgressRef.current = true;
+
+ try {
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ reconnectTimeoutRef.current = null;
+ }
+
+ const connection = codeRunnerConnections.get(roomId);
+ if (connection && currentUserIdRef.current) {
+ connection.isDestroyed = true;
+ connection.activeUsers.delete(currentUserIdRef.current);
+ console.log(
+ `[CodeRunner] μ¬μ©μ μ κ±°: ${currentUserIdRef.current}, λ¨μ μ¬μ©μ: ${connection.activeUsers.size}`
+ );
+
+ if (connection.activeUsers.size === 0) {
+ if (connection.cleanupTimer) {
+ clearTimeout(connection.cleanupTimer);
+ }
+
+ connection.cleanupTimer = setTimeout(() => {
+ cleanupCodeRunnerConnection(roomId);
+ }, 60000);
+ console.log(`[CodeRunner] cleanup timer μ€μ : ${roomId}`);
+ }
+ }
+
+ setConnectionStatus(false);
+ setIsConnected(false);
+ setError(null);
+ clearUsers();
+ currentUserIdRef.current = '';
+ } catch (cleanupError) {
+ console.error(`[CodeRunner] μ 리 μ€ μ€λ₯: ${roomId}`, cleanupError);
+ } finally {
+ isInitializedRef.current = false;
+ cleanupInProgressRef.current = false;
+ }
+ }, [roomId, setConnectionStatus, clearUsers]);
+
+ const broadcastCommand = useCallback(
+ (command: string, output: string, timestamp: Date) => {
+ const connection = codeRunnerConnections.get(roomId);
+ if (!connection || connection.isDestroyed) {
+ console.warn(`[CodeRunner] μ°κ²°μ μ°Ύμ μ μκ±°λ νκ΄΄λ¨: ${roomId}`);
+ return;
+ }
+
+ const commandData = {
+ id: `${userId}-${Date.now()}-${Math.random()}`,
+ userId,
+ userName,
+ command,
+ output,
+ timestamp: timestamp.toISOString(),
+ type: 'command',
+ };
+
+ console.log(`[CodeRunner] λͺ
λ Ήμ΄ λΈλ‘λμΊμ€νΈ:`, commandData);
+
+ try {
+ connection.yArray.push([commandData]);
+ console.log(`[CodeRunner] Y.Array νμ¬ κΈΈμ΄: ${connection.yArray.length}`);
+ } catch (error) {
+ console.error(`[CodeRunner] λΈλ‘λμΊμ€νΈ μ€ν¨:`, error);
+ }
+ },
+ [roomId, userId, userName]
+ );
+
+ const scheduleReconnect = useCallback(
+ (connection: CodeRunnerConnectionData, delay: number = 3000) => {
+ if (
+ connection.isDestroyed ||
+ connection.reconnectAttempts >= connection.maxReconnectAttempts
+ ) {
+ console.log(
+ `[CodeRunner] μ¬μ°κ²° μ€λ¨: ${roomId} (μλ: ${connection.reconnectAttempts}/${connection.maxReconnectAttempts})`
+ );
+ setError('μ°κ²°μ μ€ν¨νμ΅λλ€. νμ΄μ§λ₯Ό μλ‘κ³ μΉ¨ν΄μ£ΌμΈμ.');
+ return;
+ }
+
+ if (reconnectTimeoutRef.current) {
+ clearTimeout(reconnectTimeoutRef.current);
+ }
+
+ reconnectTimeoutRef.current = setTimeout(() => {
+ if (
+ !connection.isDestroyed &&
+ connection.provider.shouldConnect &&
+ !connection.provider.wsconnected
+ ) {
+ console.log(
+ `[CodeRunner] μ¬μ°κ²° μλ ${connection.reconnectAttempts + 1}/${connection.maxReconnectAttempts}: ${roomId}`
+ );
+ connection.reconnectAttempts++;
+ connection.provider.connect();
+ }
+ }, delay);
+ },
+ [roomId]
+ );
+
+ const initialize = useCallback(async () => {
+ if (!roomId || !enabled || isInitializedRef.current || cleanupInProgressRef.current) {
+ console.log(`[CodeRunner] μ΄κΈ°ν 건λλ°κΈ°:`, {
+ roomId,
+ enabled,
+ isInitialized: isInitializedRef.current,
+ });
+ return;
+ }
+
+ try {
+ console.log(`[CodeRunner] νμ
μ΄κΈ°ν μμ: ${roomId}, μ¬μ©μ: ${userId}(${userName})`);
+
+ currentUserIdRef.current = userId;
+
+ let connection = codeRunnerConnections.get(roomId);
+
+ if (!connection) {
+ const doc = new Y.Doc();
+ const yArray = doc.getArray('coderunner-commands');
+ const wsUrl = getWebSocketUrl();
+ const fullRoomId = `coderunner-${roomId}`;
+
+ console.log(`[CodeRunner] μ μ°κ²° μμ±: ${wsUrl}/${fullRoomId}`);
+
+ const provider = new WebsocketProvider(wsUrl, fullRoomId, doc, {
+ connect: true,
+ maxBackoffTime: 30000,
+ resyncInterval: 30000,
+ });
+
+ connection = {
+ doc,
+ provider,
+ yArray,
+ activeUsers: new Set(),
+ reconnectAttempts: 0,
+ maxReconnectAttempts: 5,
+ isDestroyed: false,
+ };
+
+ provider.on('status', (event: { status: string }) => {
+ if (connection!.isDestroyed) return;
+
+ const connected = event.status === 'connected';
+ console.log(`[CodeRunner] WebSocket μν λ³κ²½: ${event.status} (${roomId})`);
+
+ setConnectionStatus(connected);
+ setIsConnected(connected);
+
+ if (connected) {
+ setError(null);
+ connection!.reconnectAttempts = 0;
+
+ const currentUser = {
+ id: userId,
+ name: userName,
+ color: '#' + Math.floor(Math.random() * 16777215).toString(16),
+ lastSeen: Date.now(),
+ };
+
+ provider.awareness.setLocalStateField('user', currentUser);
+ console.log(`[CodeRunner] Awareness μ€μ :`, currentUser);
+ } else {
+ if (event.status === 'disconnected' && !connection!.isDestroyed) {
+ setError('μ°κ²°μ΄ λμ΄μ‘μ΅λλ€. μ¬μ°κ²° μ€...');
+ scheduleReconnect(connection!, 3000);
+ }
+ }
+ });
+
+ provider.on('connection-close', event => {
+ if (connection!.isDestroyed) return;
+ console.log(`[CodeRunner] WebSocket μ°κ²° λ«ν:`, event);
+
+ if (event && (event.code === 1003 || event.code === 1008)) {
+ console.log(`[CodeRunner] μλ²μμ μ°κ²° κ±°λΆλ¨ (μ½λ: ${event.code})`);
+ setError('μλ²μμ μ°κ²°μ κ±°λΆνμ΅λλ€.');
+ connection!.reconnectAttempts = connection!.maxReconnectAttempts;
+ } else {
+ scheduleReconnect(connection!, 5000);
+ }
+ });
+
+ provider.on('connection-error', event => {
+ if (connection!.isDestroyed) return;
+ console.error(`[CodeRunner] WebSocket μ°κ²° μ€λ₯:`, event);
+ setError('μ°κ²° μ€λ₯κ° λ°μνμ΅λλ€.');
+ });
+
+ const handleAwarenessChange = () => {
+ if (connection!.isDestroyed) return;
+
+ try {
+ const states = provider.awareness.getStates();
+ console.log(`[CodeRunner] Awareness λ³κ²½, μ΄ ν΄λΌμ΄μΈνΈ: ${states.size}`);
+
+ clearUsers();
+
+ for (const [clientId, state] of states.entries()) {
+ const awarenessState = state as {
+ user?: { id: string; name: string; color: string };
+ };
+
+ if (awarenessState.user && clientId !== provider.awareness.clientID) {
+ const user = {
+ id: awarenessState.user.id,
+ name: awarenessState.user.name,
+ color: awarenessState.user.color,
+ lastSeen: Date.now(),
+ };
+ addUser(user);
+ console.log(`[CodeRunner] μ¬μ©μ μΆκ°:`, user);
+ }
+ }
+ } catch (awarenessError) {
+ console.error('[CodeRunner] Awareness μ²λ¦¬ μ€λ₯:', awarenessError);
+ }
+ };
+
+ provider.awareness.on('change', handleAwarenessChange);
+
+ const handleYArrayChange = () => {
+ if (connection!.isDestroyed) return;
+
+ try {
+ const commands = connection!.yArray.toArray() as Array<{
+ id: string;
+ userId: string;
+ userName: string;
+ command: string;
+ output: string;
+ timestamp: string;
+ type: string;
+ }>;
+
+ console.log(`[CodeRunner] Y.Array λ³κ²½, μ΄ λͺ
λ Ήμ΄: ${commands.length}`);
+
+ // π§ μμ : μ¬μ©μ κ΅¬λΆ λ‘μ§ κ°μ
+ const formattedHistory: CommandHistory[] = commands.map(cmd => {
+ console.log(`[CodeRunner] λͺ
λ Ήμ΄ μ²λ¦¬:`, {
+ cmdUserId: cmd.userId,
+ currentUserId: userId,
+ isCurrentUser: cmd.userId === userId,
+ userName: cmd.userName,
+ command: cmd.command,
+ });
+
+ return {
+ command: cmd.command,
+ // π§ λͺ¨λ λͺ
λ Ήμ΄μ μ¬μ©μλͺ
νμ (λ³ΈμΈ λͺ
λ Ήμ΄λ κ΅¬λΆ κ°λ₯νλλ‘)
+ output: `[${cmd.userName}] ${cmd.output}`,
+ timestamp: new Date(cmd.timestamp),
+ };
+ });
+
+ setCommandHistory(formattedHistory);
+ console.log(`[CodeRunner] λͺ
λ Ήμ΄ νμ€ν 리 μ
λ°μ΄νΈ:`, formattedHistory.length);
+ } catch (arrayError) {
+ console.error('[CodeRunner] Array λ³κ²½ μ²λ¦¬ μ€λ₯:', arrayError);
+ }
+ };
+
+ connection.yArray.observe(handleYArrayChange);
+ codeRunnerConnections.set(roomId, connection);
+
+ console.log(`[CodeRunner] μ°κ²° λ§΅μ μ μ₯: ${roomId}`);
+ }
+
+ if (connection.cleanupTimer) {
+ clearTimeout(connection.cleanupTimer);
+ connection.cleanupTimer = undefined;
+ console.log(`[CodeRunner] κΈ°μ‘΄ cleanup timer μ·¨μ: ${roomId}`);
+ }
+
+ connection.activeUsers.add(userId);
+ console.log(`[CodeRunner] μ¬μ©μ μΆκ°: ${userId}, μ΄ μ¬μ©μ: ${connection.activeUsers.size}`);
+
+ const connected = connection.provider.wsconnected;
+ setConnectionStatus(connected);
+ setIsConnected(connected);
+
+ if (connected) {
+ const currentUser = {
+ id: userId,
+ name: userName,
+ color: '#' + Math.floor(Math.random() * 16777215).toString(16),
+ lastSeen: Date.now(),
+ };
+ connection.provider.awareness.setLocalStateField('user', currentUser);
+ }
+
+ isInitializedRef.current = true;
+ console.log(`[CodeRunner] νμ
μ΄κΈ°ν μλ£: ${roomId}`);
+ } catch (initError) {
+ console.error(`[CodeRunner] μ΄κΈ°ν μ€ν¨: ${roomId}`, initError);
+ setError('νμ
λͺ¨λ μ΄κΈ°νμ μ€ν¨νμ΅λλ€.');
+ cleanup();
+ }
+ }, [
+ roomId,
+ userId,
+ userName,
+ enabled,
+ setConnectionStatus,
+ addUser,
+ clearUsers,
+ cleanup,
+ scheduleReconnect,
+ ]);
+
+ useEffect(() => {
+ if (enabled && roomId && userId && userName) {
+ console.log(`[CodeRunner] μ΄κΈ°ν νΈλ¦¬κ±°:`, { roomId, userId, userName, enabled });
+ initialize();
+ } else {
+ console.log(`[CodeRunner] μ΄κΈ°ν 쑰건 λ―ΈμΆ©μ‘±:`, {
+ enabled,
+ roomId: !!roomId,
+ userId: !!userId,
+ userName: !!userName,
+ });
+ }
+
+ return cleanup;
+ }, [enabled, roomId, userId, userName, initialize, cleanup]);
+
+ return {
+ isConnected,
+ error,
+ commandHistory,
+ broadcastCommand,
+ };
+};
diff --git a/src/layouts/RepoLayout/RepoLayout.tsx b/src/layouts/RepoLayout/RepoLayout.tsx
index ced89a98..bfac9cd1 100644
--- a/src/layouts/RepoLayout/RepoLayout.tsx
+++ b/src/layouts/RepoLayout/RepoLayout.tsx
@@ -9,7 +9,7 @@ import { useThemeStore } from '@/stores/themeStore';
import { useFileSectionStore } from '@/stores/fileSectionStore';
import { useAuthStore } from '@/stores/authStore';
import { getCurrentUserId, getCurrentNickname } from '@/utils/authChatUtils';
-import Chat from '@/features/Chat/ChatStompVer';
+import Chat from '@/features/Chat/Chat';
import useStompChat from '@/hooks/chat/useStompChat';
export function RepoLayout() {
diff --git a/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx b/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx
index 1219c83a..7e4f4eb8 100644
--- a/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx
+++ b/src/pages/Main/PrivateRepoPage/PrivateRepoPage.tsx
@@ -106,6 +106,7 @@ const PrivateRepoPage = () => {
// μ’μμ νν°
const handleLikChange = () => {
setIsLiked(!isLiked);
+ setPagination(prev => ({ ...prev, current: 1 }));
repositoryRefetch();
};
diff --git a/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx b/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx
index 4488eb58..755fe103 100644
--- a/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx
+++ b/src/pages/Main/SharedByMeRepoPage/SharedByMeRepoPage.tsx
@@ -88,6 +88,7 @@ const SharedByMeRepoPage = () => {
// μ’μμ νν°
const handleLikChange = () => {
setIsLiked(!isLiked);
+ setPagination(prev => ({ ...prev, current: 1 }));
repositoryRefetch();
};
diff --git a/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx b/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx
index 42a2c00f..0f35d489 100644
--- a/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx
+++ b/src/pages/Main/SharedWithMeRepoPage/SharedWithMeRepoPage.tsx
@@ -90,6 +90,7 @@ const SharedWithMeRepoPage = () => {
// μ’μμ νν°
const handleLikChange = () => {
setIsLiked(!isLiked);
+ setPagination(prev => ({ ...prev, current: 1 }));
repositoryRefetch();
};
diff --git a/src/pages/Repo/RepoPage.module.scss b/src/pages/Repo/RepoPage.module.scss
index bb7c2afd..db70126f 100644
--- a/src/pages/Repo/RepoPage.module.scss
+++ b/src/pages/Repo/RepoPage.module.scss
@@ -150,7 +150,11 @@
flex: 0 0 273px;
box-sizing: border-box;
overflow: hidden;
- background-color: #343a40;
+ background-color: $gray-10;
+
+ &.darkMode {
+ background-color: $gray-2;
+ }
@media screen and (height <= 700px) {
flex: 0 0 200px;
diff --git a/src/pages/Repo/RepoPage.tsx b/src/pages/Repo/RepoPage.tsx
index 10068d2f..70609b4f 100644
--- a/src/pages/Repo/RepoPage.tsx
+++ b/src/pages/Repo/RepoPage.tsx
@@ -9,6 +9,7 @@ import { useCollaborationStore } from '@/stores/collaborationStore';
import { useAuthStore } from '@/stores/authStore';
import { useFileContentLoader } from '@/hooks/repo/useFileContentLoader';
import { useYjsSavePoint } from '@/hooks/repo/useYjsSavePoint';
+import { useThemeStore } from '@/stores/themeStore';
import Loading from '@/components/molecules/Loading/Loading';
import styles from './RepoPage.module.scss';
import TabBar from '@/components/organisms/TabBar/TabBar';
@@ -38,6 +39,7 @@ export function RepoPage() {
const search = useSearch({ strict: false });
const repoId = params.repoId;
const filePath = search.file;
+ const { isDarkMode } = useThemeStore();
const {
openTabs,
@@ -358,8 +360,16 @@ export function RepoPage() {
-
diff --git a/src/pages/SettingsPage/SettingsPage.module.scss b/src/pages/SettingsPage/SettingsPage.module.scss
index d0b576bf..5cff5c92 100644
--- a/src/pages/SettingsPage/SettingsPage.module.scss
+++ b/src/pages/SettingsPage/SettingsPage.module.scss
@@ -24,6 +24,10 @@
}
}
+.deleteLabel {
+ color: $red;
+}
+
.sharedByMeSettingsPage {
display: flex;
justify-content: flex-start;
diff --git a/src/pages/SettingsPage/SettingsPage.tsx b/src/pages/SettingsPage/SettingsPage.tsx
index d9f4dcf9..7abc3847 100644
--- a/src/pages/SettingsPage/SettingsPage.tsx
+++ b/src/pages/SettingsPage/SettingsPage.tsx
@@ -123,7 +123,7 @@ const SettingsPage: React.FC = () => {
onClick={() => scrollToSection('deleteSection')}
>
-
DELETE
+
DELETE
)}