Skip to content

Commit 9e930de

Browse files
authored
[Feat] 이슈 채널 봇 알림 카드 및 IssuePanel 구현
[Feat] 이슈 채널 봇 알림 카드 및 IssuePanel 구현
2 parents 3a91a19 + 2db95a5 commit 9e930de

3 files changed

Lines changed: 722 additions & 11 deletions

File tree

src/app/components/ChatPanel.tsx

Lines changed: 216 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
1-
import { Send, Sparkles, Code, AtSign, Smile, GitPullRequest, FileText, Plus, Minus, MessageSquare, Bookmark, Share2, MoreVertical, X, CheckCircle, Clock, AlertCircle, ExternalLink, GitMerge, Hash, Paperclip, FileUp, Image as ImageIcon, Link2 } from "lucide-react";
1+
import { Send, Sparkles, Code, AtSign, Smile, GitPullRequest, FileText, Plus, Minus, MessageSquare, Bookmark, Share2, MoreVertical, X, CheckCircle, Clock, AlertCircle, ExternalLink, GitMerge, Hash, Paperclip, FileUp, Image as ImageIcon, Link2, CircleDot, CircleCheck, CircleMinus } from "lucide-react";
22
import { useEffect, useRef, useState, type ChangeEvent } from "react";
33
import { createFileMessageAttachment, createLinkMessageAttachment, createLinkMessageAttachmentFromText, messageAttachmentGroups, messageAttachmentTypeLabels, type MessageAttachment, type MessageAttachmentType } from "./messageAttachments";
44
import { TypingIndicator } from "./TypingIndicator";
55
import { EmojiPicker } from "./EmojiPicker";
66
import { MessageReactions, toggleMessageReaction, type MessageReaction } from "./MessageReactions";
77
import { MessageAttachmentCard } from "./MessageAttachmentCard";
88

9+
export interface IssueLabel {
10+
name: string;
11+
color: string;
12+
}
13+
14+
export interface IssueHistoryEvent {
15+
id: string;
16+
actor: string;
17+
action: string;
18+
time: string;
19+
eventType: 'created' | 'assigned' | 'labeled' | 'commented' | 'status_changed';
20+
}
21+
922
interface Message {
1023
id: number;
1124
user: string;
1225
text: string;
1326
time: string;
14-
type?: 'text' | 'code' | 'system' | 'pr';
27+
type?: 'text' | 'code' | 'system' | 'pr' | 'issue';
1528
code?: string;
1629
language?: string;
1730
mentions?: string[];
31+
// PR fields
1832
prNumber?: number;
1933
prStatus?: 'open' | 'merged' | 'closed' | 'completed';
2034
prTitle?: string;
@@ -29,6 +43,17 @@ interface Message {
2943
aiRisk?: 'Low' | 'Medium' | 'High';
3044
passed?: number;
3145
labels?: string[];
46+
// Issue fields
47+
issueNumber?: number;
48+
issueTitle?: string;
49+
issueStatus?: 'open' | 'closed' | 'in_progress';
50+
issueAuthor?: string;
51+
issueLabels?: IssueLabel[];
52+
issuePriority?: 'high' | 'medium' | 'low';
53+
issueType?: string;
54+
issueAssignees?: string[];
55+
issueBody?: string;
56+
issueHistory?: IssueHistoryEvent[];
3257
attachments?: MessageAttachment[];
3358
}
3459

@@ -39,6 +64,7 @@ interface ChatPanelProps {
3964
showAISummary?: boolean;
4065
onMergePR?: (messageId: number) => void;
4166
onReviewPR?: (prData: any) => void;
67+
onViewIssue?: (issueData: any) => void;
4268
onOpenThread?: (message: any) => void;
4369
isRepository?: boolean;
4470
}
@@ -56,7 +82,19 @@ const shareChannels = [
5682
{ id: "design", label: "디자인" }
5783
];
5884

59-
export function ChatPanel({ title, messages, onSendMessage, showAISummary = true, onMergePR, onReviewPR, onOpenThread, isRepository = false }: ChatPanelProps) {
85+
const issueStatusConfig = {
86+
open: { label: '열림', color: '#22C55E', icon: CircleDot },
87+
in_progress: { label: '진행 중', color: 'var(--neon-cyan)', icon: Clock },
88+
closed: { label: '닫힘', color: 'var(--muted)', icon: CircleCheck },
89+
};
90+
91+
const issuePriorityConfig = {
92+
high: { label: 'High', color: '#FF6B6B' },
93+
medium: { label: 'Medium', color: '#F59E0B' },
94+
low: { label: 'Low', color: '#22C55E' },
95+
};
96+
97+
export function ChatPanel({ title, messages, onSendMessage, showAISummary = true, onMergePR, onReviewPR, onViewIssue, onOpenThread, isRepository = false }: ChatPanelProps) {
6098
const [message, setMessage] = useState('');
6199
const [showCodeBlock, setShowCodeBlock] = useState(false);
62100
const [activeTab, setActiveTab] = useState<'all' | 'pending' | 'completed'>('all');
@@ -584,6 +622,181 @@ export function ChatPanel({ title, messages, onSendMessage, showAISummary = true
584622
</div>
585623
)}
586624
</div>
625+
) : msg.type === 'issue' ? (
626+
<div className="relative">
627+
<div
628+
role="button"
629+
tabIndex={0}
630+
onClick={() => onViewIssue?.(msg)}
631+
onKeyDown={(e) => {
632+
if (e.key === 'Enter' || e.key === ' ') {
633+
e.preventDefault();
634+
onViewIssue?.(msg);
635+
}
636+
}}
637+
className="rounded-xl overflow-hidden transition-all hover:translate-y-[-1px]"
638+
style={{
639+
background: 'rgba(11, 22, 40, 0.85)',
640+
border: '1px solid rgba(34, 197, 94, 0.22)',
641+
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.3)',
642+
cursor: 'pointer'
643+
}}
644+
>
645+
{/* Header */}
646+
<div className="px-4 py-2.5 flex items-center justify-between" style={{
647+
background: 'rgba(5, 11, 20, 0.5)',
648+
borderBottom: '1px solid rgba(34, 197, 94, 0.14)'
649+
}}>
650+
<div className="flex items-center gap-2">
651+
<CircleDot size={14} style={{ color: '#22C55E' }} />
652+
<span className="font-mono tracking-tight" style={{
653+
fontSize: '11px',
654+
fontWeight: 800,
655+
color: 'var(--muted)'
656+
}}>
657+
GitHub Issues
658+
</span>
659+
</div>
660+
<div className="flex items-center gap-1.5 flex-wrap justify-end">
661+
{msg.issueLabels?.map((label, idx) => (
662+
<span key={idx} className="px-2 py-0.5 rounded-md tracking-tight" style={{
663+
background: `${label.color}22`,
664+
border: `1px solid ${label.color}66`,
665+
fontSize: '10px',
666+
fontWeight: 800,
667+
color: label.color
668+
}}>
669+
{label.name}
670+
</span>
671+
))}
672+
</div>
673+
</div>
674+
675+
{/* Title */}
676+
<div className="px-4 py-3" style={{
677+
borderBottom: '1px solid rgba(34, 197, 94, 0.10)'
678+
}}>
679+
<h4 className="m-0 mb-2 tracking-tight" style={{
680+
fontSize: '15px',
681+
fontWeight: 950,
682+
color: 'var(--white)',
683+
lineHeight: '1.4'
684+
}}>
685+
#{msg.issueNumber} {msg.issueTitle}
686+
</h4>
687+
<div className="flex items-center gap-2 flex-wrap">
688+
{msg.issueStatus && (() => {
689+
const cfg = issueStatusConfig[msg.issueStatus];
690+
const Icon = cfg.icon;
691+
return (
692+
<span className="px-2 py-0.5 rounded-md flex items-center gap-1" style={{
693+
background: `${cfg.color}22`,
694+
border: `1px solid ${cfg.color}44`,
695+
fontSize: '10px',
696+
fontWeight: 900,
697+
color: cfg.color
698+
}}>
699+
<Icon size={10} />
700+
{cfg.label}
701+
</span>
702+
);
703+
})()}
704+
<span className="tracking-tight" style={{
705+
fontSize: '11px',
706+
fontWeight: 700,
707+
color: 'var(--muted)'
708+
}}>
709+
{msg.time}
710+
</span>
711+
<span className="tracking-tight" style={{
712+
fontSize: '11px',
713+
fontWeight: 700,
714+
color: 'var(--muted)'
715+
}}>
716+
작성자 {msg.issueAuthor || msg.user}
717+
</span>
718+
</div>
719+
</div>
720+
721+
{/* Actions */}
722+
<div className="px-4 py-3 flex items-center gap-2 flex-wrap">
723+
{msg.issuePriority && (
724+
<span className="px-2.5 py-1 rounded-md tracking-tight" style={{
725+
background: `${issuePriorityConfig[msg.issuePriority].color}22`,
726+
border: `1px solid ${issuePriorityConfig[msg.issuePriority].color}44`,
727+
fontSize: '10px',
728+
fontWeight: 900,
729+
color: issuePriorityConfig[msg.issuePriority].color
730+
}}>
731+
우선순위: {issuePriorityConfig[msg.issuePriority].label}
732+
</span>
733+
)}
734+
{msg.issueType && (
735+
<span className="px-2.5 py-1 rounded-md tracking-tight" style={{
736+
background: 'rgba(234, 247, 255, 0.07)',
737+
border: '1px solid rgba(234, 247, 255, 0.14)',
738+
fontSize: '10px',
739+
fontWeight: 900,
740+
color: 'var(--muted)'
741+
}}>
742+
{msg.issueType}
743+
</span>
744+
)}
745+
{msg.issueAssignees && msg.issueAssignees.length > 0 && (
746+
<span className="tracking-tight" style={{
747+
fontSize: '11px',
748+
fontWeight: 700,
749+
color: 'var(--muted)'
750+
}}>
751+
담당자: {msg.issueAssignees.join(', ')}
752+
</span>
753+
)}
754+
<button
755+
onClick={(e) => {
756+
e.stopPropagation();
757+
onViewIssue?.(msg);
758+
}}
759+
className="ml-auto px-3 py-1.5 rounded-md border-0 tracking-tight transition-all flex items-center gap-1.5"
760+
style={{
761+
background: 'rgba(34, 197, 94, 0.12)',
762+
border: '1px solid rgba(34, 197, 94, 0.28)',
763+
color: '#22C55E',
764+
fontSize: '11px',
765+
fontWeight: 900,
766+
cursor: 'pointer'
767+
}}
768+
>
769+
이슈 열기
770+
</button>
771+
</div>
772+
</div>
773+
{hoveredMessageId === msg.id && (
774+
<div className="absolute top-2 right-2 flex items-center gap-1 px-2 py-1 rounded-lg" style={{
775+
background: 'rgba(11, 22, 40, 0.95)',
776+
border: '1px solid rgba(32, 227, 255, 0.3)',
777+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.5)'
778+
}}>
779+
<button
780+
onClick={() => onOpenThread?.(msg)}
781+
className="w-7 h-7 rounded border-0 flex items-center justify-center transition-all hover:bg-[rgba(32,227,255,0.15)]"
782+
style={{ background: 'transparent', color: 'var(--muted)', cursor: 'pointer' }}
783+
title="답글"
784+
>
785+
<MessageSquare size={14} />
786+
</button>
787+
<button className="w-7 h-7 rounded border-0 flex items-center justify-center transition-all hover:bg-[rgba(32,227,255,0.15)]" style={{
788+
background: 'transparent', color: 'var(--muted)', cursor: 'pointer'
789+
}} title="북마크">
790+
<Bookmark size={14} />
791+
</button>
792+
<button className="w-7 h-7 rounded border-0 flex items-center justify-center transition-all hover:bg-[rgba(32,227,255,0.15)]" style={{
793+
background: 'transparent', color: 'var(--muted)', cursor: 'pointer'
794+
}} title="더보기">
795+
<MoreVertical size={14} />
796+
</button>
797+
</div>
798+
)}
799+
</div>
587800
) : msg.type === 'code' && msg.code ? (
588801
<div className="relative">
589802
<div className="rounded-xl overflow-hidden" style={{

0 commit comments

Comments
 (0)