Skip to content

Commit c885128

Browse files
authored
Merge pull request #23 from prgrms-aibe-devcourse/feat/22-sidebar-repo-management
[Feat] 사이드바 레포지토리 추가 및 삭제 기능 복원
2 parents 778c60c + 3f43f64 commit c885128

1 file changed

Lines changed: 315 additions & 0 deletions

File tree

src/app/pages/ChatPage.tsx

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ export function ChatPage() {
360360
const [selectedRepository, setSelectedRepository] = useState<string>(() =>
361361
getRepositoryImportPreference() ? getSavedRepositories()?.[0]?.id ?? DEFAULT_REPOSITORIES[0].id : ""
362362
);
363+
const [showRepoDropdown, setShowRepoDropdown] = useState(false);
364+
const [showRepoForm, setShowRepoForm] = useState(false);
365+
const [repoUrlInput, setRepoUrlInput] = useState('');
363366
const [selectedChannel, setSelectedChannel] = useState<string>('overview');
364367
const [messages, setMessages] = useState<Record<string, any[]>>(initialMessages);
365368
const [selectedPR, setSelectedPR] = useState<any>(null);
@@ -388,6 +391,7 @@ export function ChatPage() {
388391
const navigate = useNavigate();
389392

390393
const hasRepositories = repositoriesImported && repositories.length > 0;
394+
const currentRepo = repositories.find(repo => repo.id === selectedRepository);
391395

392396
const getChannelBadge = (channelId: string): string | undefined => {
393397
const count = channelUnreadCounts[channelId];
@@ -449,6 +453,64 @@ export function ChatPage() {
449453
});
450454
}, [selectedChannel]);
451455

456+
const parseRepoNameFromUrl = (url: string): string | null => {
457+
try {
458+
const trimmed = url.trim().replace(/\.git$/, '');
459+
const parts = trimmed.split('/').filter(Boolean);
460+
const name = parts[parts.length - 1];
461+
return name || null;
462+
} catch {
463+
return null;
464+
}
465+
};
466+
467+
const handleOpenRepoForm = () => {
468+
setShowRepoDropdown(false);
469+
setShowRepoForm(true);
470+
setRepoUrlInput('');
471+
};
472+
473+
const handleCloseRepoForm = () => {
474+
setShowRepoForm(false);
475+
setRepoUrlInput('');
476+
};
477+
478+
const handleSubmitRepoForm = () => {
479+
const repoName = parseRepoNameFromUrl(repoUrlInput);
480+
if (!repoName) return;
481+
const nextRepository: RepositoryItem = {
482+
id: `repo-${Date.now()}`,
483+
name: repoName,
484+
openPRs: 0,
485+
highRisk: 0,
486+
activeIssues: 0,
487+
connected: true,
488+
membersOnline: 1
489+
};
490+
setRepositories(prev => [nextRepository, ...prev]);
491+
setRepositoriesImported(true);
492+
setSelectedRepository(nextRepository.id);
493+
setSelectedChannel('overview');
494+
handleCloseRepoForm();
495+
};
496+
497+
const handleDeleteRepository = (repositoryId: string) => {
498+
const nextRepositories = repositories.filter((repo) => repo.id !== repositoryId);
499+
setRepositories(nextRepositories);
500+
if (selectedRepository === repositoryId) {
501+
setSelectedRepository(nextRepositories[0]?.id ?? "");
502+
}
503+
if (nextRepositories.length === 0) {
504+
setRepositoriesImported(false);
505+
setSelectedChannel('overview');
506+
setShowRepoDropdown(false);
507+
saveRepositoryImportPreferenceValue(false);
508+
saveRepositories([]);
509+
return;
510+
}
511+
saveRepositories(nextRepositories);
512+
};
513+
452514
const handleAddCustomChannel = () => {
453515
setShowAddChannelForm(true);
454516
setNewChannelName('');
@@ -756,6 +818,259 @@ export function ChatPage() {
756818
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.32)',
757819
backdropFilter: 'blur(16px)'
758820
}}>
821+
<div className="mb-4">
822+
{hasRepositories ? (
823+
<div className="relative">
824+
<button
825+
onClick={() => setShowRepoDropdown(!showRepoDropdown)}
826+
className="w-full px-4 py-3 rounded-lg border-0 flex items-center justify-between gap-2 transition-all"
827+
style={{
828+
background: 'rgba(32, 227, 255, 0.12)',
829+
border: '1px solid rgba(32, 227, 255, 0.3)',
830+
cursor: 'pointer'
831+
}}
832+
>
833+
<div className="flex items-center gap-3 flex-1 min-w-0">
834+
<div className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0" style={{
835+
background: 'linear-gradient(135deg, var(--neon-cyan), var(--deep-teal))'
836+
}}>
837+
<GitBranch size={14} style={{ color: '#021014' }} />
838+
</div>
839+
<span className="tracking-tight truncate" style={{
840+
fontSize: '14px',
841+
fontWeight: 900,
842+
color: 'var(--white)'
843+
}}>
844+
{currentRepo?.name}
845+
</span>
846+
</div>
847+
<ChevronDown size={16} style={{ color: 'var(--neon-cyan)', flexShrink: 0 }} />
848+
</button>
849+
850+
<AnimatePresence initial={false}>
851+
{showRepoDropdown && (
852+
<motion.div
853+
className="absolute top-full left-0 right-0 mt-2 rounded-lg overflow-hidden z-10"
854+
style={{
855+
background: 'rgba(5, 11, 20, 0.95)',
856+
border: '1px solid rgba(32, 227, 255, 0.3)',
857+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.5)'
858+
}}
859+
initial={{ opacity: 0, y: -8, height: 0 }}
860+
animate={{ opacity: 1, y: 0, height: 'auto' }}
861+
exit={{ opacity: 0, y: -8, height: 0 }}
862+
transition={{ type: 'spring', stiffness: 360, damping: 32 }}
863+
>
864+
{repositories.map((repo) => (
865+
<div
866+
key={repo.id}
867+
className="flex items-stretch gap-2 px-3 py-3"
868+
style={{
869+
background: selectedRepository === repo.id ? 'rgba(32, 227, 255, 0.15)' : 'transparent',
870+
borderBottom: '1px solid rgba(32, 227, 255, 0.1)'
871+
}}
872+
>
873+
<button
874+
type="button"
875+
onClick={() => {
876+
setSelectedRepository(repo.id);
877+
setShowRepoDropdown(false);
878+
}}
879+
className="min-w-0 flex-1 border-0 bg-transparent p-0 text-left"
880+
style={{ cursor: 'pointer' }}
881+
>
882+
<div className="flex flex-col gap-2">
883+
<span className="truncate tracking-tight" style={{
884+
fontSize: '14px',
885+
fontWeight: selectedRepository === repo.id ? 900 : 800,
886+
color: selectedRepository === repo.id ? 'var(--neon-cyan)' : 'var(--white)'
887+
}}>
888+
{repo.name}
889+
</span>
890+
<div className="flex flex-wrap gap-3">
891+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: 'var(--muted)' }}>
892+
진행 중인 PR: <span style={{ color: 'var(--neon-cyan)' }}>{repo.openPRs}</span>
893+
</span>
894+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: 'var(--muted)' }}>
895+
높은 위험: <span style={{ color: repo.highRisk > 0 ? '#FF6B6B' : 'var(--matrix-green)' }}>{repo.highRisk}</span>
896+
</span>
897+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: 'var(--muted)' }}>
898+
이슈: <span style={{ color: 'var(--soft-mint)' }}>{repo.activeIssues}</span>
899+
</span>
900+
</div>
901+
</div>
902+
</button>
903+
<div className="flex shrink-0 items-center">
904+
<button
905+
type="button"
906+
onClick={() => handleDeleteRepository(repo.id)}
907+
className="grid h-8 w-8 place-items-center rounded-full border-0 transition-all hover:scale-105"
908+
style={{
909+
background: 'rgba(255, 107, 107, 0.10)',
910+
border: '1px solid rgba(255, 107, 107, 0.22)',
911+
color: '#FF6B6B',
912+
cursor: 'pointer'
913+
}}
914+
aria-label={`${repo.name} 삭제`}
915+
title="삭제"
916+
>
917+
<Trash2 size={14} />
918+
</button>
919+
</div>
920+
</div>
921+
))}
922+
<div className="px-3 py-3">
923+
<button
924+
type="button"
925+
onClick={handleOpenRepoForm}
926+
className="flex w-full items-center justify-center gap-2 rounded-full border-0 px-4 py-3 tracking-tight transition-all hover:scale-[1.01]"
927+
style={{
928+
background: 'rgba(57, 255, 136, 0.12)',
929+
border: '1px solid rgba(57, 255, 136, 0.22)',
930+
color: 'var(--matrix-green)',
931+
cursor: 'pointer',
932+
fontSize: '12px',
933+
fontWeight: 950
934+
}}
935+
>
936+
<Plus size={15} />
937+
리포지토리 추가
938+
</button>
939+
</div>
940+
</motion.div>
941+
)}
942+
</AnimatePresence>
943+
</div>
944+
) : (
945+
<div
946+
className="rounded-2xl px-4 py-4"
947+
style={{
948+
background: 'rgba(234, 247, 255, 0.045)',
949+
border: '1px solid rgba(32, 227, 255, 0.16)'
950+
}}
951+
>
952+
<div className="flex items-center gap-3 flex-1 min-w-0">
953+
<div className="w-7 h-7 rounded-lg flex items-center justify-center flex-shrink-0" style={{
954+
background: 'linear-gradient(135deg, var(--neon-cyan), var(--deep-teal))'
955+
}}>
956+
<GitBranch size={14} style={{ color: '#021014' }} />
957+
</div>
958+
<div className="flex flex-col items-start min-w-0">
959+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 900, color: 'var(--muted)' }}>선택한 팀</span>
960+
<span className="tracking-tight truncate" style={{ fontSize: '14px', fontWeight: 900, color: 'var(--white)' }}>리포지토리 없음</span>
961+
</div>
962+
</div>
963+
</div>
964+
)}
965+
966+
<AnimatePresence initial={false}>
967+
{showRepoForm && (
968+
<motion.div
969+
className="mt-4 rounded-2xl px-4 py-4"
970+
style={{
971+
background: 'rgba(5, 11, 20, 0.58)',
972+
border: '1px solid rgba(32, 227, 255, 0.18)',
973+
boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.06)'
974+
}}
975+
initial={{ opacity: 0, y: -8, height: 0 }}
976+
animate={{ opacity: 1, y: 0, height: 'auto' }}
977+
exit={{ opacity: 0, y: -8, height: 0 }}
978+
transition={{ type: 'spring', stiffness: 360, damping: 32 }}
979+
>
980+
<div className="mb-3 flex items-center justify-between gap-3">
981+
<div className="min-w-0">
982+
<p className="m-0 tracking-tight" style={{ color: 'var(--white)', fontSize: '13px', fontWeight: 950 }}>
983+
리포지토리 추가
984+
</p>
985+
<p className="m-0 mt-1 tracking-tight" style={{ color: 'var(--muted)', fontSize: '11px', fontWeight: 800 }}>
986+
GitHub 저장소 URL을 입력하세요
987+
</p>
988+
</div>
989+
<button
990+
type="button"
991+
onClick={handleCloseRepoForm}
992+
className="grid h-8 w-8 shrink-0 place-items-center rounded-full border-0"
993+
style={{ background: 'rgba(234, 247, 255, 0.07)', color: 'var(--muted)', cursor: 'pointer' }}
994+
aria-label="닫기"
995+
>
996+
<X size={15} />
997+
</button>
998+
</div>
999+
1000+
<input
1001+
value={repoUrlInput}
1002+
onChange={(e) => setRepoUrlInput(e.target.value)}
1003+
onKeyDown={(e) => {
1004+
if (e.key === 'Enter') { e.preventDefault(); handleSubmitRepoForm(); }
1005+
if (e.key === 'Escape') { e.preventDefault(); handleCloseRepoForm(); }
1006+
}}
1007+
placeholder="https://github.com/owner/repository"
1008+
className="w-full rounded-xl px-4 py-3 outline-none tracking-tight"
1009+
style={{
1010+
background: 'rgba(234, 247, 255, 0.08)',
1011+
border: '1px solid rgba(32, 227, 255, 0.22)',
1012+
color: 'var(--white)',
1013+
fontSize: '13px',
1014+
fontWeight: 850
1015+
}}
1016+
/>
1017+
1018+
<div className="mt-3 flex items-center gap-2">
1019+
<button
1020+
type="button"
1021+
onClick={handleCloseRepoForm}
1022+
className="flex-1 rounded-full border-0 px-4 py-2.5 tracking-tight"
1023+
style={{
1024+
background: 'rgba(234, 247, 255, 0.07)',
1025+
border: '1px solid rgba(32, 227, 255, 0.12)',
1026+
color: 'var(--muted)',
1027+
cursor: 'pointer',
1028+
fontSize: '12px',
1029+
fontWeight: 900
1030+
}}
1031+
>
1032+
취소
1033+
</button>
1034+
<button
1035+
type="button"
1036+
onClick={handleSubmitRepoForm}
1037+
disabled={!parseRepoNameFromUrl(repoUrlInput)}
1038+
className="flex flex-1 items-center justify-center gap-2 rounded-full border-0 px-4 py-2.5 tracking-tight transition-all disabled:opacity-40"
1039+
style={{
1040+
background: 'linear-gradient(135deg, var(--neon-cyan), var(--deep-teal))',
1041+
color: '#021014',
1042+
cursor: parseRepoNameFromUrl(repoUrlInput) ? 'pointer' : 'not-allowed',
1043+
fontSize: '12px',
1044+
fontWeight: 950
1045+
}}
1046+
>
1047+
<Plus size={14} />
1048+
등록
1049+
</button>
1050+
</div>
1051+
</motion.div>
1052+
)}
1053+
</AnimatePresence>
1054+
</div>
1055+
1056+
{hasRepositories && (
1057+
<div className="mt-3 mb-2 flex items-center gap-2 px-2">
1058+
<div className="flex items-center gap-2">
1059+
<div className="w-2 h-2 rounded-full" style={{ background: currentRepo?.connected ? 'var(--matrix-green)' : 'var(--muted)' }} />
1060+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: currentRepo?.connected ? 'var(--matrix-green)' : 'var(--muted)' }}>
1061+
{currentRepo?.connected ? 'GitHub 연결됨' : '연결되지 않음'}
1062+
</span>
1063+
</div>
1064+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: 'var(--muted)' }}></span>
1065+
<div className="flex items-center gap-2">
1066+
<div className="w-2 h-2 rounded-full" style={{ background: 'var(--matrix-green)' }} />
1067+
<span className="tracking-tight" style={{ fontSize: '11px', fontWeight: 800, color: 'var(--muted)' }}>
1068+
{currentRepo?.membersOnline}명 접속 중
1069+
</span>
1070+
</div>
1071+
</div>
1072+
)}
1073+
7591074
{hasRepositories ? (
7601075
<div className="flex flex-1 flex-col overflow-y-auto">
7611076
<div className="grid gap-2">

0 commit comments

Comments
 (0)