Skip to content

Commit b52970a

Browse files
authored
Merge pull request #217 from prgrms-aibe-devcourse/fix/repository-channel-stomp-routing
fix: repository channel STOMP events routed to issues tab
2 parents fb16977 + 9226fff commit b52970a

1 file changed

Lines changed: 27 additions & 185 deletions

File tree

src/app/pages/ChatPage.tsx

Lines changed: 27 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,8 @@ import type { ChatStompClient } from "../api/stomp";
6464
import { getAccessToken } from "../auth";
6565
import { useProfile } from "../contexts/ProfileContext";
6666
import { fetchMyWorkspaces, getWorkspaceMembers, updatePresence, type WorkspaceDto, type WorkspaceMember } from "../api/workspace";
67-
import { type WorkspaceEventDto } from "../api/events";
6867
import { useWorkspace } from "../contexts/WorkspaceContext";
6968
import { ApiClientError } from "../api/client";
70-
import { connectWorkspaceRepository, fetchWorkspaceRepositories, syncRepositoryIssueStatuses } from "../api/github";
7169

7270
const APISpecPage = lazy(() => import("./APISpecPage").then((module) => ({ default: module.APISpecPage })));
7371
const ERDPage = lazy(() => import("./ERDPage").then((module) => ({ default: module.ERDPage })));
@@ -135,8 +133,6 @@ interface RepositoryItem {
135133
connected: boolean;
136134
membersOnline: number;
137135
workspaceId?: string;
138-
channelId?: number; // 백엔드 repository channel DB id
139-
dbRepoId?: string; // github_repositories DB id
140136
}
141137

142138
interface WorkspaceItem {
@@ -573,29 +569,6 @@ function mapChannelMessageToWorkspaceMessage(message: ChannelMessage) {
573569
// The backend marks soft-deleted messages by replacing the content with a sentinel string.
574570
// Detect it here so a page refresh keeps the message in the "deleted" state (no edit/delete buttons).
575571
const isDeleted = message.isDeleted === true || message.content === DELETED_MESSAGE_CONTENT;
576-
const replyTo = message.replyTo
577-
? { user: message.replyTo.senderName ?? "", text: message.replyTo.content }
578-
: undefined;
579-
580-
// GitHub bot issue notification — parse meta JSON from the issue attachment
581-
const issueAttachment = (message.attachments ?? []).find(
582-
(a) => a.attachmentType === "issue" || a.type === "issue"
583-
);
584-
let issueFields: Record<string, unknown> = {};
585-
if (issueAttachment?.meta) {
586-
try {
587-
const parsed = JSON.parse(issueAttachment.meta);
588-
// 우선순위 미설정 시 기본값 'medium'
589-
if (!parsed.issuePriority) parsed.issuePriority = 'medium';
590-
// 담당자 미설정 시 작성자로 fallback
591-
if (!parsed.issueAssignees || (parsed.issueAssignees as unknown[]).length === 0) {
592-
parsed.issueAssignees = parsed.issueAuthor ? [parsed.issueAuthor] : [];
593-
}
594-
issueFields = { type: "issue", ...parsed };
595-
} catch {
596-
// meta가 JSON이 아닌 경우 무시
597-
}
598-
}
599572

600573
return {
601574
id: message.id,
@@ -609,14 +582,11 @@ function mapChannelMessageToWorkspaceMessage(message: ChannelMessage) {
609582
time: formatApiDateTime(message.createdAt),
610583
replies: 0,
611584
attachments,
612-
...(replyTo ? { replyTo } : {}),
613-
...(isDeleted ? { deleted: true } : {}),
614-
...issueFields,
585+
...(isDeleted ? { deleted: true } : {})
615586
};
616587
}
617588

618589
function mapThreadReplyToWorkspaceMessage(reply: ThreadReply) {
619-
const isDeleted = reply.isDeleted === true || reply.content === DELETED_MESSAGE_CONTENT;
620590
return {
621591
id: reply.id,
622592
backendReplyId: reply.id,
@@ -626,8 +596,7 @@ function mapThreadReplyToWorkspaceMessage(reply: ThreadReply) {
626596
avatar: reply.senderName?.charAt(0).toUpperCase() || "U",
627597
text: reply.content,
628598
message: reply.content,
629-
time: formatApiDateTime(reply.createdAt),
630-
...(isDeleted ? { deleted: true } : {})
599+
time: formatApiDateTime(reply.createdAt)
631600
};
632601
}
633602

@@ -1095,15 +1064,11 @@ export function ChatPage() {
10951064
const [workspaceMembers, setWorkspaceMembers] = useState<WorkspaceMember[]>([]);
10961065

10971066
useEffect(() => {
1098-
const state = location.state as { workspaceId?: string | number; pendingEvent?: WorkspaceEventDto } | null;
1099-
const pendingEvent = state?.pendingEvent ?? null;
1100-
if (pendingEvent) pendingEventRef.current = pendingEvent;
1101-
11021067
fetchMyWorkspaces()
11031068
.then((workspaces) => {
11041069
setApiWorkspaces(workspaces);
11051070
if (workspaces.length > 0) {
1106-
const incomingId = pendingEvent?.workspaceId ?? state?.workspaceId;
1071+
const incomingId = (location.state as { workspaceId?: string | number } | null)?.workspaceId;
11071072
const incomingApiId = parseWorkspaceApiId(incomingId);
11081073
const target = incomingApiId !== null
11091074
? workspaces.find((w) => w.id === incomingApiId)
@@ -1179,7 +1144,6 @@ export function ChatPage() {
11791144
);
11801145
const [isMainExpanded, setIsMainExpanded] = useState(false);
11811146
const prevMainExpanded = useRef(false);
1182-
const pendingEventRef = useRef<WorkspaceEventDto | null>(null);
11831147
const [teamInviteOpen, setTeamInviteOpen] = useState(false);
11841148
const [expandedSidebarGroups, setExpandedSidebarGroups] = useState<Record<SidebarGroupId, boolean>>({
11851149
documentation: true
@@ -1258,6 +1222,7 @@ export function ChatPage() {
12581222

12591223

12601224
const hasRepositories = repositoriesImported && repositories.length > 0;
1225+
const currentRepo = repositories.find(repo => repo.id === selectedRepository);
12611226
const currentWorkspace = workspaceList.find(ws => ws.id === selectedWorkspace) ?? workspaceList[0];
12621227
const canManageWorkspaceChannels = canManageWorkspaceChannel(currentWorkspace?.myRole);
12631228
const currentWorkspaceApiId = getWorkspaceApiId(selectedWorkspace, currentWorkspace);
@@ -1350,41 +1315,6 @@ export function ChatPage() {
13501315
return channels;
13511316
});
13521317
}, [currentWorkspaceApiId]);
1353-
1354-
// 백엔드 레포 목록으로 channelId 동기화 (기존 localStorage 항목 업데이트)
1355-
useEffect(() => {
1356-
if (!currentWorkspaceApiId || currentWorkspaceApiId <= 0) return;
1357-
fetchWorkspaceRepositories(currentWorkspaceApiId).then((list) => {
1358-
if (!list.length) return;
1359-
1360-
// 기존 이슈 상태 동기화 (DB meta와 실제 GitHub 이슈 상태 일치)
1361-
list.forEach((repo) => {
1362-
syncRepositoryIssueStatuses(String(repo.id)).catch(() => { /* ignore */ });
1363-
});
1364-
1365-
const raw = window.localStorage.getItem(WORKSPACE_REPOS_KEY);
1366-
if (!raw) return;
1367-
try {
1368-
const parsed = JSON.parse(raw);
1369-
if (!Array.isArray(parsed)) return;
1370-
const wsId = String(currentWorkspaceApiId);
1371-
let changed = false;
1372-
const updated = parsed.map((r) => {
1373-
if (r.workspaceId !== wsId) return r;
1374-
const match = list.find(
1375-
(b) => r.dbRepoId === String(b.id) || r.name === b.name
1376-
);
1377-
if (match && (r.channelId !== match.channelId || r.dbRepoId !== String(match.id))) {
1378-
changed = true;
1379-
return { ...r, channelId: match.channelId, dbRepoId: String(match.id) };
1380-
}
1381-
return r;
1382-
});
1383-
if (changed) window.localStorage.setItem(WORKSPACE_REPOS_KEY, JSON.stringify(updated));
1384-
} catch { /* ignore */ }
1385-
}).catch(() => { /* ignore */ });
1386-
}, [currentWorkspaceApiId]);
1387-
13881318
const visibleRepositories = useMemo(() => {
13891319
try {
13901320
const raw = window.localStorage.getItem(WORKSPACE_REPOS_KEY);
@@ -1405,8 +1335,6 @@ export function ChatPage() {
14051335
connected: true,
14061336
membersOnline: 0,
14071337
workspaceId: r.workspaceId,
1408-
channelId: r.channelId,
1409-
dbRepoId: r.dbRepoId,
14101338
}));
14111339
}
14121340
}
@@ -1416,8 +1344,6 @@ export function ChatPage() {
14161344
}
14171345
return [];
14181346
}, [currentWorkspaceApiId]);
1419-
const currentRepo = visibleRepositories.find(repo => repo.id === selectedRepository)
1420-
?? repositories.find(repo => repo.id === selectedRepository);
14211347
const firstVisibleRepositoryId = visibleRepositories[0]?.id ?? null;
14221348
const apiChannelIdByUiChannel = useMemo(() => {
14231349
return apiChannels.reduce<Record<string, number>>((acc, channel) => {
@@ -1431,11 +1357,7 @@ export function ChatPage() {
14311357
return acc;
14321358
}, {});
14331359
}, [apiChannels]);
1434-
// 'issues' 탭은 현재 레포의 repository channel(DB id)로 매핑
1435-
const activeApiChannelId =
1436-
selectedChannel === 'issues' && currentRepo?.channelId
1437-
? currentRepo.channelId
1438-
: apiChannelIdByUiChannel[selectedChannel];
1360+
const activeApiChannelId = apiChannelIdByUiChannel[selectedChannel];
14391361
const hasActiveApiChatChannel = activeApiChannelId !== undefined;
14401362
const hasChatAccessToken = Boolean(getAccessToken());
14411363
const realtimeConnectionBlockReason = useMemo<RealtimeConnectionReason | null>(() => {
@@ -1454,7 +1376,7 @@ export function ChatPage() {
14541376
]);
14551377
const apiCustomChannels = useMemo<CustomChannelItem[]>(() => {
14561378
return apiChannels
1457-
.filter((channel) => getApiChannelUiId(channel) !== "general" && channel.channelType !== "repository")
1379+
.filter((channel) => getApiChannelUiId(channel) !== "general")
14581380
.map((channel) => ({
14591381
id: getApiChannelUiId(channel),
14601382
label: cleanChannelLabel(channel.name),
@@ -2538,7 +2460,6 @@ export function ChatPage() {
25382460
setChatStompReadyKey((key) => key + 1);
25392461

25402462
eventSubscriptions = apiChannels.map((channel) => {
2541-
// Repository channels map to the 'issues' UI tab, not to the generic api-ch-{id} key
25422463
const uiChannelId = String(channel.channelType ?? "").toLowerCase() === "repository"
25432464
? "issues"
25442465
: getApiChannelUiId(channel);
@@ -2798,50 +2719,24 @@ export function ChatPage() {
27982719
setRepoUrlInput('');
27992720
};
28002721

2801-
const handleSubmitRepoForm = async () => {
2802-
const trimmed = repoUrlInput.trim().replace(/\.git$/, '');
2803-
const parts = trimmed.split('/').filter(Boolean);
2804-
const repoName = parts[parts.length - 1];
2805-
const owner = parts[parts.length - 2];
2806-
if (!repoName || !owner) return;
2807-
2808-
try {
2809-
const res = await connectWorkspaceRepository(currentWorkspaceApiId, owner, repoName);
2810-
const nextRepository: RepositoryItem = {
2811-
id: `repo-${res.id}`,
2812-
name: res.name,
2813-
openPRs: 0,
2814-
highRisk: 0,
2815-
activeIssues: 0,
2816-
connected: true,
2817-
membersOnline: 1,
2818-
workspaceId: selectedWorkspace,
2819-
channelId: res.channelId,
2820-
dbRepoId: String(res.id),
2821-
};
2822-
setRepositories(prev => [nextRepository, ...prev]);
2823-
setRepositoriesImported(true);
2824-
setSelectedRepository(nextRepository.id);
2825-
setSelectedChannel('overview');
2826-
handleCloseRepoForm();
2827-
} catch {
2828-
// 백엔드 연동 실패 시 로컬 목데이터로 폴백
2829-
const nextRepository: RepositoryItem = {
2830-
id: `repo-${Date.now()}`,
2831-
name: repoName,
2832-
openPRs: 0,
2833-
highRisk: 0,
2834-
activeIssues: 0,
2835-
connected: true,
2836-
membersOnline: 1,
2837-
workspaceId: selectedWorkspace,
2838-
};
2839-
setRepositories(prev => [nextRepository, ...prev]);
2840-
setRepositoriesImported(true);
2841-
setSelectedRepository(nextRepository.id);
2842-
setSelectedChannel('overview');
2843-
handleCloseRepoForm();
2844-
}
2722+
const handleSubmitRepoForm = () => {
2723+
const repoName = parseRepoNameFromUrl(repoUrlInput);
2724+
if (!repoName) return;
2725+
const nextRepository: RepositoryItem = {
2726+
id: `repo-${Date.now()}`,
2727+
name: repoName,
2728+
openPRs: 0,
2729+
highRisk: 0,
2730+
activeIssues: 0,
2731+
connected: true,
2732+
membersOnline: 1,
2733+
workspaceId: selectedWorkspace
2734+
};
2735+
setRepositories(prev => [nextRepository, ...prev]);
2736+
setRepositoriesImported(true);
2737+
setSelectedRepository(nextRepository.id);
2738+
setSelectedChannel('overview');
2739+
handleCloseRepoForm();
28452740
};
28462741

28472742
const handleDeleteRepository = (repositoryId: string) => {
@@ -3590,56 +3485,6 @@ export function ChatPage() {
35903485
setSelectedThread(null);
35913486
};
35923487

3593-
// pendingEvent: 레포 목록 변경 시 레포·채널 선택
3594-
useEffect(() => {
3595-
const pending = pendingEventRef.current;
3596-
if (!pending) return;
3597-
if (pending.type === "MENTION" || pending.type === "REPLY") return; // 채널 처리는 아래에서
3598-
if (!pending.repositoryName) return;
3599-
const repo = repositories.find((r) => r.name === pending.repositoryName);
3600-
if (!repo) return;
3601-
setSelectedRepository(repo.id);
3602-
if (pending.type === "PR_CREATED" || pending.type === "PR_REVIEW") {
3603-
setSelectedChannel("pull-requests");
3604-
} else if (pending.type === "ISSUE_CREATED") {
3605-
setSelectedChannel("issues");
3606-
}
3607-
}, [repositories]);
3608-
3609-
// pendingEvent: apiChannels 변경 시 MENTION·REPLY 채널 선택
3610-
useEffect(() => {
3611-
const pending = pendingEventRef.current;
3612-
if (!pending || (pending.type !== "MENTION" && pending.type !== "REPLY")) return;
3613-
if (!pending.channelId) return;
3614-
const uiChannelId = apiChannelUiById[pending.channelId];
3615-
if (uiChannelId) setSelectedChannel(uiChannelId);
3616-
}, [apiChannelUiById]);
3617-
3618-
// pendingEvent: 메시지 로드 후 PR·이슈·스레드 패널 열기
3619-
useEffect(() => {
3620-
const pending = pendingEventRef.current;
3621-
if (!pending) return;
3622-
if (pending.type === "PR_CREATED" || pending.type === "PR_REVIEW") {
3623-
if (!pending.prNumber) return;
3624-
const msg = currentMessages.find((m: any) => m.type === "pr" && m.prNumber === pending.prNumber);
3625-
if (!msg) return;
3626-
handleReviewPR(msg);
3627-
pendingEventRef.current = null;
3628-
} else if (pending.type === "ISSUE_CREATED") {
3629-
if (!pending.issueNumber) return;
3630-
const msg = currentMessages.find((m: any) => m.type === "issue" && m.issueNumber === pending.issueNumber);
3631-
if (!msg) return;
3632-
handleViewIssue(msg);
3633-
pendingEventRef.current = null;
3634-
} else if (pending.type === "MENTION" || pending.type === "REPLY") {
3635-
if (!pending.threadId) return;
3636-
const msg = currentMessages.find((m: any) => Number(m.backendMessageId ?? m.id) === pending.threadId);
3637-
if (!msg) return;
3638-
handleOpenThread(msg);
3639-
pendingEventRef.current = null;
3640-
}
3641-
}, [currentMessages]);
3642-
36433488
const getReactionTarget = (reactionKey: string) => {
36443489
if (reactionKey.endsWith(":original") && selectedThread) {
36453490
return {
@@ -3746,7 +3591,7 @@ export function ChatPage() {
37463591
});
37473592
};
37483593

3749-
const handleSendMessage = (text: string, attachments: MessageAttachment[] = [], replyTo?: { user: string; text: string; messageId?: number }, metadata?: MessageMetadata) => {
3594+
const handleSendMessage = (text: string, attachments: MessageAttachment[] = [], replyTo?: { user: string; text: string }, metadata?: MessageMetadata) => {
37503595
const trimmedText = text.trim();
37513596
if (!trimmedText && attachments.length === 0) return;
37523597
if (attachments.length > 10) return;
@@ -3755,7 +3600,6 @@ export function ChatPage() {
37553600
const pendingMessageId = Date.now();
37563601
const attachmentPayload = attachments.map(toMessageAttachmentRequest);
37573602
const messageText = trimmedText || `${attachments.length}개 항목을 공유합니다.`;
3758-
const replyToMessageId = replyTo?.messageId;
37593603

37603604
const nextMessage: any = {
37613605
id: pendingMessageId,
@@ -3790,8 +3634,7 @@ export function ChatPage() {
37903634
stompClient.send(
37913635
chatWebSocketDestinations.sendChannelMessage(activeApiChannelId),
37923636
{
3793-
content: messageText,
3794-
...(replyToMessageId ? { replyToMessageId } : {})
3637+
content: messageText
37953638
}
37963639
);
37973640
return;
@@ -3802,8 +3645,7 @@ export function ChatPage() {
38023645

38033646
createChannelMessage(activeApiChannelId, {
38043647
content: messageText,
3805-
...(attachmentPayload.length > 0 ? { attachments: attachmentPayload } : {}),
3806-
...(replyToMessageId ? { replyToMessageId } : {})
3648+
...(attachmentPayload.length > 0 ? { attachments: attachmentPayload } : {})
38073649
})
38083650
.then((serverMessage) => appendServerMessage(selectedChannel, serverMessage))
38093651
.catch((error) => {

0 commit comments

Comments
 (0)