Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 58 additions & 2 deletions src/features/Chat/components/ChatMessage/ChatMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import { useParams, useNavigate } from '@tanstack/react-router';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { useTabStore } from '@/stores/tabStore';
import './ChatMessage.scss';
import { type ChatReceivedMessage } from '@/features/Chat/types';

Expand All @@ -15,12 +17,51 @@ dayjs.extend(timezone);
dayjs.locale('ko');

const ChatMessage: React.FC<ChatMessageProps> = ({ message, isMyMessage }) => {
const params = useParams({ strict: false });
const navigate = useNavigate();
const { openFileByPath } = useTabStore();

// 현재 repo ID 가져오기
const repoId = params.repoId as string;

// 시간 포맷팅 함수
const formatTime = (isoString: string) => {
const time = dayjs.utc(isoString).tz('Asia/Seoul').format('HH:mm');
return time;
};

// 파일 경로 클릭 핸들러
const handleFilePathClick = (filePath: string) => {
if (!repoId) {
console.warn('repoId가 없어서 파일을 열 수 없습니다.');
return;
}

// 파일명 추출 (경로의 마지막 부분)
const fileName = filePath.includes('/') ? filePath.split('/').pop() || 'untitled' : filePath;

console.log('채팅에서 파일 경로 클릭:', {
repoId,
filePath,
fileName,
});

// 탭으로 파일 열기
openFileByPath(repoId, filePath, fileName);

// URL 업데이트하여 파일 경로 반영
try {
navigate({
to: '/$repoId',
params: { repoId },
search: { file: filePath },
replace: false,
});
} catch (error) {
console.error('파일 경로 네비게이션 실패:', error);
}
};

// 메시지 내용에서 코드 참조 파싱
const renderMessageContent = (content: string) => {
// [[Ref: 파일 경로]] 패턴
Expand All @@ -35,9 +76,24 @@ const ChatMessage: React.FC<ChatMessageProps> = ({ message, isMyMessage }) => {
parts.push(content.slice(lastIndex, match.index));
}

const filePath = match[1].trim();

parts.push(
<span key={match.index} className="chat-message__reference">
[[Ref: {match[1]}]]
<span
key={match.index}
className="chat-message__reference"
onClick={() => handleFilePathClick(filePath)}
role="button"
tabIndex={0}
onKeyDown={e => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleFilePathClick(filePath);
}
}}
title={`${filePath} 파일 열기`}
>
[[Ref: {filePath}]]
</span>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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};
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -38,6 +39,29 @@ const FileTreeItem: React.FC<FileTreeItemProps> = ({
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) {
Expand Down Expand Up @@ -242,6 +266,10 @@ const FileTreeItem: React.FC<FileTreeItemProps> = ({
[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',
Expand Down Expand Up @@ -270,6 +298,11 @@ const FileTreeItem: React.FC<FileTreeItemProps> = ({
onDrop={handleCombinedDrop}
// 최상단 레벨 여부를 data attribute로 전달
data-is-top-level={isTopLevel}
title={
tabStatus.isOpen
? `${node.fileName}${tabStatus.isActive ? ' (활성 탭)' : ' (열린 탭)'}${tabStatus.isDirty ? ' (변경됨)' : ''}`
: node.fileName
}
>
<div className={styles.arrowArea}>
{node.fileType === 'FOLDER' && (
Expand Down Expand Up @@ -309,6 +342,16 @@ const FileTreeItem: React.FC<FileTreeItemProps> = ({
validateInput={validateFileName}
/>

{/* 탭 상태 인디케이터 - 우측 끝에 고정 위치 */}
{node.fileType === 'FILE' && (
<div className={styles.tabStatusIndicators}>
{tabStatus.isActive && <div className={styles.activeIndicator} title="현재 활성 탭" />}
{tabStatus.isDirty && (
<div className={styles.dirtyIndicator} title="변경된 내용이 있음" />
)}
</div>
)}

{/* 외부 파일 드래그오버 상태 표시 */}
{isExternalDragOver && (
<div className={styles.externalDropIndicator}>
Expand Down