@@ -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 ( / \. g i t $ / , '' ) ;
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