1- import { Hash , Users , GitPullRequest , Home , CheckSquare , ChevronDown , ChevronRight , GitBranch , Code2 , Database , BookOpen , Maximize2 , Minimize2 , Plus , Pencil , Trash2 , MoreVertical , X , LayoutGrid , type LucideIcon } from "lucide-react" ;
1+ import { Hash , Users , GitPullRequest , Home , CheckSquare , ChevronDown , ChevronRight , GitBranch , Code2 , Database , BookOpen , Maximize2 , Minimize2 , Plus , Pencil , Trash2 , MoreVertical , X , LayoutGrid , Bell , BellOff , Check , Clock3 , MessageCircle , Settings , UserRound , type LucideIcon } from "lucide-react" ;
22import { WorkBoardPanel } from "../components/WorkBoardPanel" ;
33import { ChatPanel } from "../components/ChatPanel" ;
44import { PRReviewPanel } from "../components/PRReviewPanel" ;
@@ -25,6 +25,8 @@ const CHAT_THREAD_REPLY_COUNTS_KEY = "codedock-chat-thread-reply-counts-v1";
2525const CHAT_REACTIONS_KEY = "codedock-chat-reactions-v1" ;
2626
2727type SidebarGroupId = 'documentation' ;
28+ type UserPresence = 'active' | 'away' | 'busy' | 'offline' ;
29+ type NotificationMode = 'all' | 'mentions' | 'muted' ;
2830
2931interface RepositoryItem {
3032 id : string ;
@@ -100,6 +102,26 @@ const ALL_SIDEBAR_CHANNELS = [
100102 ...DOCUMENTATION_CHANNELS
101103] ;
102104
105+ const myProfile = {
106+ name : "김준우" ,
107+ role : "Frontend Developer" ,
108+ email : "junwoo@codedock.dev" ,
109+ initials : "JW"
110+ } ;
111+
112+ const presenceOptions : Array < { id : UserPresence ; label : string ; description : string ; color : string } > = [
113+ { id : 'active' , label : '활동중' , description : '바로 응답 가능' , color : '#39FF88' } ,
114+ { id : 'away' , label : '자리비움' , description : '잠시 후 확인' , color : '#FFD166' } ,
115+ { id : 'busy' , label : '방해금지' , description : '멘션만 확인' , color : '#FF6B6B' } ,
116+ { id : 'offline' , label : '오프라인' , description : '상태 숨김' , color : '#8B94A7' }
117+ ] ;
118+
119+ const notificationOptions : Array < { id : NotificationMode ; label : string ; description : string ; icon : LucideIcon } > = [
120+ { id : 'all' , label : '모든 알림' , description : '채널, PR, 이슈 알림 받기' , icon : Bell } ,
121+ { id : 'mentions' , label : '멘션만' , description : '@멘션과 배정 알림만 받기' , icon : MessageCircle } ,
122+ { id : 'muted' , label : '알림 끄기' , description : '새 알림을 조용히 보관' , icon : BellOff }
123+ ] ;
124+
103125function getRepositoryImportPreference ( ) {
104126 if ( typeof window === "undefined" || typeof window . localStorage === "undefined" ) {
105127 return false ;
@@ -528,6 +550,9 @@ export function ChatPage() {
528550 'backend-chat' : 1 ,
529551 'review-room' : 2 ,
530552 } ) ;
553+ const [ profileMenuOpen , setProfileMenuOpen ] = useState ( false ) ;
554+ const [ userPresence , setUserPresence ] = useState < UserPresence > ( 'active' ) ;
555+ const [ notificationMode , setNotificationMode ] = useState < NotificationMode > ( 'mentions' ) ;
531556
532557 const [ selectedWorkspace , setSelectedWorkspace ] = useState < string > ( DEFAULT_WORKSPACES [ 0 ] . id ) ;
533558
@@ -579,6 +604,10 @@ export function ChatPage() {
579604 ?? selectedChannel . split ( '-' ) . map ( word => word . charAt ( 0 ) . toUpperCase ( ) + word . slice ( 1 ) ) . join ( ' ' ) ;
580605 const selectedRepositoryName = repositories . find ( ( repo ) => repo . id === selectedRepository ) ?. name ?? '전체 리포지토리' ;
581606
607+ const currentPresence = presenceOptions . find ( ( option ) => option . id === userPresence ) ?? presenceOptions [ 0 ] ;
608+ const currentNotificationMode = notificationOptions . find ( ( option ) => option . id === notificationMode ) ?? notificationOptions [ 0 ] ;
609+ const CurrentNotificationIcon = currentNotificationMode . icon ;
610+
582611 useEffect ( ( ) => {
583612 if ( ! isMainExpanded ) return ;
584613
@@ -901,6 +930,196 @@ export function ChatPage() {
901930 ) ;
902931 } ;
903932
933+ const renderProfileDock = ( ) => (
934+ < div className = "relative" >
935+ < AnimatePresence initial = { false } >
936+ { profileMenuOpen && (
937+ < motion . div
938+ className = "absolute bottom-full left-0 right-0 mb-3 overflow-hidden rounded-2xl px-3 py-3"
939+ style = { {
940+ background : 'rgba(5, 11, 20, 0.98)' ,
941+ border : '1px solid rgba(32, 227, 255, 0.22)' ,
942+ boxShadow : '0 20px 56px rgba(0, 0, 0, 0.48), 0 0 30px rgba(32, 227, 255, 0.12)' ,
943+ backdropFilter : 'blur(18px) saturate(180%)' ,
944+ zIndex : 30
945+ } }
946+ initial = { { opacity : 0 , y : 10 , scale : 0.98 } }
947+ animate = { { opacity : 1 , y : 0 , scale : 1 } }
948+ exit = { { opacity : 0 , y : 10 , scale : 0.98 } }
949+ transition = { { type : 'spring' , stiffness : 420 , damping : 34 } }
950+ >
951+ < div className = "mb-3 px-1" >
952+ < p className = "m-0 tracking-tight" style = { { color : 'var(--white)' , fontSize : '13px' , fontWeight : 950 } } >
953+ 내 상태
954+ </ p >
955+ < p className = "m-0 mt-1 tracking-tight" style = { { color : 'var(--muted)' , fontSize : '11px' , fontWeight : 800 } } >
956+ 팀원에게 표시되는 상태를 바꿉니다
957+ </ p >
958+ </ div >
959+
960+ < div className = "grid gap-1.5" >
961+ { presenceOptions . map ( ( option ) => {
962+ const selected = option . id === userPresence ;
963+ return (
964+ < button
965+ key = { option . id }
966+ type = "button"
967+ onClick = { ( ) => setUserPresence ( option . id ) }
968+ className = "flex w-full items-center gap-3 rounded-xl border-0 px-3 py-2.5 text-left tracking-tight"
969+ style = { {
970+ background : selected ? 'rgba(32, 227, 255, 0.12)' : 'transparent' ,
971+ border : selected ? '1px solid rgba(32, 227, 255, 0.20)' : '1px solid transparent' ,
972+ cursor : 'pointer'
973+ } }
974+ >
975+ < span className = "h-2.5 w-2.5 flex-shrink-0 rounded-full" style = { { background : option . color } } />
976+ < span className = "min-w-0 flex-1" >
977+ < span className = "block truncate" style = { { color : 'var(--white)' , fontSize : '12px' , fontWeight : 950 } } >
978+ { option . label }
979+ </ span >
980+ < span className = "block truncate" style = { { color : 'var(--muted)' , fontSize : '10px' , fontWeight : 800 } } >
981+ { option . description }
982+ </ span >
983+ </ span >
984+ { selected && < Check size = { 14 } style = { { color : 'var(--neon-cyan)' , flexShrink : 0 } } /> }
985+ </ button >
986+ ) ;
987+ } ) }
988+ </ div >
989+
990+ < div className = "my-3" style = { { borderTop : '1px solid rgba(32, 227, 255, 0.14)' } } />
991+
992+ < div className = "mb-2 px-1" >
993+ < p className = "m-0 tracking-tight" style = { { color : 'var(--white)' , fontSize : '13px' , fontWeight : 950 } } >
994+ 알림 설정
995+ </ p >
996+ </ div >
997+
998+ < div className = "grid gap-1.5" >
999+ { notificationOptions . map ( ( option ) => {
1000+ const selected = option . id === notificationMode ;
1001+ const Icon = option . icon ;
1002+ return (
1003+ < button
1004+ key = { option . id }
1005+ type = "button"
1006+ onClick = { ( ) => setNotificationMode ( option . id ) }
1007+ className = "flex w-full items-center gap-3 rounded-xl border-0 px-3 py-2.5 text-left tracking-tight"
1008+ style = { {
1009+ background : selected ? 'rgba(57, 255, 136, 0.10)' : 'transparent' ,
1010+ border : selected ? '1px solid rgba(57, 255, 136, 0.18)' : '1px solid transparent' ,
1011+ cursor : 'pointer'
1012+ } }
1013+ >
1014+ < Icon size = { 15 } style = { { color : selected ? 'var(--matrix-green)' : 'var(--muted)' , flexShrink : 0 } } />
1015+ < span className = "min-w-0 flex-1" >
1016+ < span className = "block truncate" style = { { color : 'var(--white)' , fontSize : '12px' , fontWeight : 950 } } >
1017+ { option . label }
1018+ </ span >
1019+ < span className = "block truncate" style = { { color : 'var(--muted)' , fontSize : '10px' , fontWeight : 800 } } >
1020+ { option . description }
1021+ </ span >
1022+ </ span >
1023+ { selected && < Check size = { 14 } style = { { color : 'var(--matrix-green)' , flexShrink : 0 } } /> }
1024+ </ button >
1025+ ) ;
1026+ } ) }
1027+ </ div >
1028+
1029+ < div className = "my-3" style = { { borderTop : '1px solid rgba(32, 227, 255, 0.14)' } } />
1030+
1031+ < div className = "grid grid-cols-2 gap-2" >
1032+ < button
1033+ type = "button"
1034+ onClick = { ( ) => {
1035+ setProfileMenuOpen ( false ) ;
1036+ navigate ( '/profile' ) ;
1037+ } }
1038+ className = "flex items-center justify-center gap-2 rounded-xl border-0 px-3 py-2.5 tracking-tight"
1039+ style = { {
1040+ background : 'rgba(234, 247, 255, 0.07)' ,
1041+ border : '1px solid rgba(32, 227, 255, 0.14)' ,
1042+ color : 'var(--white)' ,
1043+ cursor : 'pointer' ,
1044+ fontSize : '12px' ,
1045+ fontWeight : 900
1046+ } }
1047+ >
1048+ < UserRound size = { 14 } />
1049+ 프로필
1050+ </ button >
1051+ < button
1052+ type = "button"
1053+ onClick = { ( ) => {
1054+ setProfileMenuOpen ( false ) ;
1055+ navigate ( '/settings' ) ;
1056+ } }
1057+ className = "flex items-center justify-center gap-2 rounded-xl border-0 px-3 py-2.5 tracking-tight"
1058+ style = { {
1059+ background : 'rgba(234, 247, 255, 0.07)' ,
1060+ border : '1px solid rgba(32, 227, 255, 0.14)' ,
1061+ color : 'var(--white)' ,
1062+ cursor : 'pointer' ,
1063+ fontSize : '12px' ,
1064+ fontWeight : 900
1065+ } }
1066+ >
1067+ < Settings size = { 14 } />
1068+ 설정
1069+ </ button >
1070+ </ div >
1071+ </ motion . div >
1072+ ) }
1073+ </ AnimatePresence >
1074+
1075+ < button
1076+ type = "button"
1077+ onClick = { ( ) => setProfileMenuOpen ( ( open ) => ! open ) }
1078+ className = "flex w-full items-center gap-3 rounded-2xl border-0 px-3 py-3 text-left tracking-tight transition-all"
1079+ style = { {
1080+ background : profileMenuOpen
1081+ ? 'linear-gradient(135deg, rgba(32, 227, 255, 0.16), rgba(57, 255, 136, 0.08)), rgba(11, 22, 40, 0.88)'
1082+ : 'rgba(5, 11, 20, 0.72)' ,
1083+ border : profileMenuOpen ? '1px solid rgba(32, 227, 255, 0.34)' : '1px solid rgba(32, 227, 255, 0.18)' ,
1084+ boxShadow : profileMenuOpen ? '0 0 28px rgba(32, 227, 255, 0.14)' : 'inset 0 1px 0 rgba(255, 255, 255, 0.06)' ,
1085+ cursor : 'pointer'
1086+ } }
1087+ aria-expanded = { profileMenuOpen }
1088+ aria-label = "내 프로필 메뉴 열기"
1089+ >
1090+ < span className = "relative grid h-10 w-10 flex-shrink-0 place-items-center rounded-full" style = { {
1091+ background : 'linear-gradient(135deg, var(--neon-cyan), var(--matrix-green))' ,
1092+ color : '#021014' ,
1093+ fontSize : '13px' ,
1094+ fontWeight : 950
1095+ } } >
1096+ { myProfile . initials }
1097+ < span className = "absolute -bottom-0.5 -right-0.5 h-3.5 w-3.5 rounded-full" style = { {
1098+ background : currentPresence . color ,
1099+ border : '2px solid #07111f'
1100+ } } />
1101+ </ span >
1102+ < span className = "min-w-0 flex-1" >
1103+ < span className = "block truncate" style = { { color : 'var(--white)' , fontSize : '13px' , fontWeight : 950 } } >
1104+ { myProfile . name }
1105+ </ span >
1106+ < span className = "mt-0.5 flex min-w-0 items-center gap-1.5" >
1107+ < Clock3 size = { 11 } style = { { color : currentPresence . color , flexShrink : 0 } } />
1108+ < span className = "truncate" style = { { color : 'var(--muted)' , fontSize : '11px' , fontWeight : 850 } } >
1109+ { currentPresence . label }
1110+ </ span >
1111+ </ span >
1112+ </ span >
1113+ < span className = "grid h-8 w-8 flex-shrink-0 place-items-center rounded-full" style = { {
1114+ background : notificationMode === 'muted' ? 'rgba(255, 107, 107, 0.10)' : 'rgba(32, 227, 255, 0.10)' ,
1115+ border : notificationMode === 'muted' ? '1px solid rgba(255, 107, 107, 0.22)' : '1px solid rgba(32, 227, 255, 0.16)'
1116+ } } >
1117+ < CurrentNotificationIcon size = { 14 } style = { { color : notificationMode === 'muted' ? '#FF8FA3' : 'var(--neon-cyan)' } } />
1118+ </ span >
1119+ </ button >
1120+ </ div >
1121+ ) ;
1122+
9041123 const handleMergePR = ( messageId : number ) => {
9051124 setMessages ( prevMessages => {
9061125 const newMessages = { ...prevMessages } ;
@@ -1213,8 +1432,8 @@ export function ChatPage() {
12131432 ) }
12141433
12151434 { visibleRepositories . length > 0 ? (
1216- < div className = "flex flex-1 flex-col overflow-y-auto " >
1217- < div className = "grid gap-2 min-w-0 " >
1435+ < div className = "flex flex-1 flex-col overflow-hidden " >
1436+ < div className = "grid min-w-0 flex-1 content-start gap-2 overflow-y-auto pr-1 " >
12181437 { renderSidebarChannel ( { id : 'overview' , label : '통합 개요' , icon : Home } ) }
12191438
12201439 < div className = "my-1" style = { { borderTop : '1px solid rgba(32, 227, 255, 0.14)' } } />
@@ -1753,6 +1972,7 @@ export function ChatPage() {
17531972 < div className = "mb-2" style = { { borderTop : '1px solid rgba(32, 227, 255, 0.14)' } } > </ div >
17541973
17551974 { renderSidebarChannel ( { id : 'team' , label : '팀' , icon : Users } ) }
1975+ { renderProfileDock ( ) }
17561976 </ div >
17571977 </ div >
17581978 ) : (
0 commit comments